mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[APM] useFetcher: Replace react-redux-request with hooks and Context API (#33392)
This commit is contained in:
parent
a7062883d2
commit
30c63f57fd
88 changed files with 3122 additions and 2921 deletions
|
@ -293,6 +293,7 @@
|
||||||
"@types/json5": "^0.0.30",
|
"@types/json5": "^0.0.30",
|
||||||
"@types/listr": "^0.13.0",
|
"@types/listr": "^0.13.0",
|
||||||
"@types/lodash": "^3.10.1",
|
"@types/lodash": "^3.10.1",
|
||||||
|
"@types/lru-cache": "^5.1.0",
|
||||||
"@types/minimatch": "^2.0.29",
|
"@types/minimatch": "^2.0.29",
|
||||||
"@types/mocha": "^5.2.6",
|
"@types/mocha": "^5.2.6",
|
||||||
"@types/moment-timezone": "^0.5.8",
|
"@types/moment-timezone": "^0.5.8",
|
||||||
|
|
|
@ -44,8 +44,8 @@
|
||||||
"@types/d3-array": "^1.2.1",
|
"@types/d3-array": "^1.2.1",
|
||||||
"@types/d3-scale": "^2.0.0",
|
"@types/d3-scale": "^2.0.0",
|
||||||
"@types/d3-shape": "^1.3.1",
|
"@types/d3-shape": "^1.3.1",
|
||||||
"@types/d3-time": "^1.0.7",
|
|
||||||
"@types/d3-time-format": "^2.1.0",
|
"@types/d3-time-format": "^2.1.0",
|
||||||
|
"@types/d3-time": "^1.0.7",
|
||||||
"@types/elasticsearch": "^5.0.30",
|
"@types/elasticsearch": "^5.0.30",
|
||||||
"@types/graphql": "^0.13.1",
|
"@types/graphql": "^0.13.1",
|
||||||
"@types/history": "^4.6.2",
|
"@types/history": "^4.6.2",
|
||||||
|
@ -55,12 +55,13 @@
|
||||||
"@types/jsonwebtoken": "^7.2.7",
|
"@types/jsonwebtoken": "^7.2.7",
|
||||||
"@types/lodash": "^3.10.1",
|
"@types/lodash": "^3.10.1",
|
||||||
"@types/mocha": "^5.2.6",
|
"@types/mocha": "^5.2.6",
|
||||||
|
"@types/object-hash": "^1.2.0",
|
||||||
"@types/pngjs": "^3.3.1",
|
"@types/pngjs": "^3.3.1",
|
||||||
"@types/prop-types": "^15.5.3",
|
"@types/prop-types": "^15.5.3",
|
||||||
"@types/react": "^16.8.0",
|
|
||||||
"@types/react-dom": "^16.8.0",
|
"@types/react-dom": "^16.8.0",
|
||||||
"@types/react-redux": "^6.0.6",
|
"@types/react-redux": "^6.0.6",
|
||||||
"@types/react-router-dom": "^4.3.1",
|
"@types/react-router-dom": "^4.3.1",
|
||||||
|
"@types/react": "^16.8.0",
|
||||||
"@types/recompose": "^0.30.2",
|
"@types/recompose": "^0.30.2",
|
||||||
"@types/reduce-reducers": "^0.1.3",
|
"@types/reduce-reducers": "^0.1.3",
|
||||||
"@types/sinon": "^5.0.1",
|
"@types/sinon": "^5.0.1",
|
||||||
|
@ -115,7 +116,9 @@
|
||||||
"proxyquire": "1.7.11",
|
"proxyquire": "1.7.11",
|
||||||
"react-docgen-typescript-loader": "^3.0.0",
|
"react-docgen-typescript-loader": "^3.0.0",
|
||||||
"react-docgen-typescript-webpack-plugin": "^1.1.0",
|
"react-docgen-typescript-webpack-plugin": "^1.1.0",
|
||||||
|
"react-hooks-testing-library": "^0.3.8",
|
||||||
"react-test-renderer": "^16.8.0",
|
"react-test-renderer": "^16.8.0",
|
||||||
|
"react-testing-library": "^6.0.0",
|
||||||
"redux-test-utils": "0.2.2",
|
"redux-test-utils": "0.2.2",
|
||||||
"rsync": "0.4.0",
|
"rsync": "0.4.0",
|
||||||
"run-sequence": "^2.2.1",
|
"run-sequence": "^2.2.1",
|
||||||
|
@ -226,6 +229,7 @@
|
||||||
"ngreact": "^0.5.1",
|
"ngreact": "^0.5.1",
|
||||||
"node-fetch": "^2.1.2",
|
"node-fetch": "^2.1.2",
|
||||||
"nodemailer": "^4.6.4",
|
"nodemailer": "^4.6.4",
|
||||||
|
"object-hash": "^1.3.1",
|
||||||
"object-path-immutable": "^0.5.3",
|
"object-path-immutable": "^0.5.3",
|
||||||
"oppsy": "^2.0.0",
|
"oppsy": "^2.0.0",
|
||||||
"papaparse": "^4.6.0",
|
"papaparse": "^4.6.0",
|
||||||
|
|
|
@ -15,10 +15,14 @@ function getUiSettingsClient() {
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unexpected config key: ${key}`);
|
throw new Error(`Unexpected config key: ${key}`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBasePath() {
|
||||||
|
return '/some/base/path';
|
||||||
|
}
|
||||||
|
|
||||||
function addBasePath(path) {
|
function addBasePath(path) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +47,7 @@ function getXsrfToken() {
|
||||||
export default {
|
export default {
|
||||||
getInjected,
|
getInjected,
|
||||||
addBasePath,
|
addBasePath,
|
||||||
|
getBasePath,
|
||||||
getUiSettingsClient,
|
getUiSettingsClient,
|
||||||
getXsrfToken
|
getXsrfToken,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,6 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RRRRenderResponse } from 'react-redux-request';
|
|
||||||
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
|
||||||
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/ui/APMError';
|
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/ui/APMError';
|
||||||
import { mockMoment } from '../../../../utils/testHelpers';
|
import { mockMoment } from '../../../../utils/testHelpers';
|
||||||
import { DetailView } from './index';
|
import { DetailView } from './index';
|
||||||
|
@ -31,10 +29,7 @@ describe('DetailView', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render Discover button', () => {
|
it('should render Discover button', () => {
|
||||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
const errorGroup = {
|
||||||
args: [],
|
|
||||||
status: 'SUCCESS',
|
|
||||||
data: {
|
|
||||||
occurrencesCount: 10,
|
occurrencesCount: 10,
|
||||||
error: ({
|
error: ({
|
||||||
'@timestamp': 'myTimestamp',
|
'@timestamp': 'myTimestamp',
|
||||||
|
@ -45,7 +40,6 @@ describe('DetailView', () => {
|
||||||
error: { exception: { handled: true } },
|
error: { exception: { handled: true } },
|
||||||
transaction: { id: 'myTransactionId', sampled: true }
|
transaction: { id: 'myTransactionId', sampled: true }
|
||||||
} as unknown) as APMError
|
} as unknown) as APMError
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<DetailView
|
<DetailView
|
||||||
|
@ -60,13 +54,9 @@ describe('DetailView', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render StickyProperties', () => {
|
it('should render StickyProperties', () => {
|
||||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
const errorGroup = {
|
||||||
args: [],
|
|
||||||
status: 'SUCCESS',
|
|
||||||
data: {
|
|
||||||
occurrencesCount: 10,
|
occurrencesCount: 10,
|
||||||
error: {} as APMError
|
error: {} as APMError
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<DetailView
|
<DetailView
|
||||||
|
@ -80,17 +70,13 @@ describe('DetailView', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render tabs', () => {
|
it('should render tabs', () => {
|
||||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
const errorGroup = {
|
||||||
args: [],
|
|
||||||
status: 'SUCCESS',
|
|
||||||
data: {
|
|
||||||
occurrencesCount: 10,
|
occurrencesCount: 10,
|
||||||
error: ({
|
error: ({
|
||||||
'@timestamp': 'myTimestamp',
|
'@timestamp': 'myTimestamp',
|
||||||
service: {},
|
service: {},
|
||||||
user: {}
|
user: {}
|
||||||
} as unknown) as APMError
|
} as unknown) as APMError
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<DetailView
|
<DetailView
|
||||||
|
@ -105,16 +91,12 @@ describe('DetailView', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render TabContent', () => {
|
it('should render TabContent', () => {
|
||||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
const errorGroup = {
|
||||||
args: [],
|
|
||||||
status: 'SUCCESS',
|
|
||||||
data: {
|
|
||||||
occurrencesCount: 10,
|
occurrencesCount: 10,
|
||||||
error: ({
|
error: ({
|
||||||
'@timestamp': 'myTimestamp',
|
'@timestamp': 'myTimestamp',
|
||||||
context: {}
|
context: {}
|
||||||
} as unknown) as APMError
|
} as unknown) as APMError
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<DetailView
|
<DetailView
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RRRRenderResponse } from 'react-redux-request';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { idx } from 'x-pack/plugins/apm/common/idx';
|
import { idx } from 'x-pack/plugins/apm/common/idx';
|
||||||
import {
|
import {
|
||||||
|
@ -24,7 +23,6 @@ import {
|
||||||
history,
|
history,
|
||||||
toQuery
|
toQuery
|
||||||
} from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
|
} from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
|
||||||
import { STATUS } from 'x-pack/plugins/apm/public/constants';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
||||||
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/ui/APMError';
|
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/ui/APMError';
|
||||||
|
@ -49,16 +47,13 @@ const HeaderContainer = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
errorGroup: RRRRenderResponse<ErrorGroupAPIResponse>;
|
errorGroup: ErrorGroupAPIResponse;
|
||||||
urlParams: IUrlParams;
|
urlParams: IUrlParams;
|
||||||
location: Location;
|
location: Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailView({ errorGroup, urlParams, location }: Props) {
|
export function DetailView({ errorGroup, urlParams, location }: Props) {
|
||||||
if (errorGroup.status !== STATUS.SUCCESS) {
|
const { transaction, error, occurrencesCount } = errorGroup;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { transaction, error, occurrencesCount } = errorGroup.data;
|
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -20,8 +20,11 @@ import React, { Fragment } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { NOT_AVAILABLE_LABEL } from 'x-pack/plugins/apm/common/i18n';
|
import { NOT_AVAILABLE_LABEL } from 'x-pack/plugins/apm/common/i18n';
|
||||||
import { idx } from 'x-pack/plugins/apm/common/idx';
|
import { idx } from 'x-pack/plugins/apm/common/idx';
|
||||||
import { ErrorDistributionRequest } from '../../../store/reactReduxRequest/errorDistribution';
|
import { useFetcher } from '../../../hooks/useFetcher';
|
||||||
import { ErrorGroupDetailsRequest } from '../../../store/reactReduxRequest/errorGroup';
|
import {
|
||||||
|
loadErrorDistribution,
|
||||||
|
loadErrorGroupDetails
|
||||||
|
} from '../../../services/rest/apm/error_groups';
|
||||||
import { IUrlParams } from '../../../store/urlParams';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
|
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -64,21 +67,32 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorGroupDetailsView({ urlParams, location }: Props) {
|
export function ErrorGroupDetailsView({ urlParams, location }: Props) {
|
||||||
return (
|
const { serviceName, start, end, errorGroupId } = urlParams;
|
||||||
<ErrorGroupDetailsRequest
|
|
||||||
urlParams={urlParams}
|
const { data: errorGroupData } = useFetcher(
|
||||||
render={errorGroup => {
|
() => loadErrorGroupDetails({ serviceName, start, end, errorGroupId }),
|
||||||
// If there are 0 occurrences, show only distribution chart w. empty message
|
[serviceName, start, end, errorGroupId]
|
||||||
const showDetails = errorGroup.data.occurrencesCount !== 0;
|
|
||||||
const logMessage = idx(errorGroup, _ => _.data.error.error.log.message);
|
|
||||||
const excMessage = idx(
|
|
||||||
errorGroup,
|
|
||||||
_ => _.data.error.error.exception[0].message
|
|
||||||
);
|
);
|
||||||
const culprit = idx(errorGroup, _ => _.data.error.error.culprit);
|
|
||||||
|
const { data: errorDistributionData } = useFetcher(
|
||||||
|
() => loadErrorDistribution({ serviceName, start, end }),
|
||||||
|
[serviceName, start, end]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!errorGroupData || !errorDistributionData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are 0 occurrences, show only distribution chart w. empty message
|
||||||
|
const showDetails = errorGroupData.occurrencesCount !== 0;
|
||||||
|
const logMessage = idx(errorGroupData, _ => _.error.error.log.message);
|
||||||
|
const excMessage = idx(
|
||||||
|
errorGroupData,
|
||||||
|
_ => _.error.error.exception[0].message
|
||||||
|
);
|
||||||
|
const culprit = idx(errorGroupData, _ => _.error.error.culprit);
|
||||||
const isUnhandled =
|
const isUnhandled =
|
||||||
idx(errorGroup, _ => _.data.error.error.exception[0].handled) ===
|
idx(errorGroupData, _ => _.error.error.exception[0].handled) === false;
|
||||||
false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -86,36 +100,28 @@ export function ErrorGroupDetailsView({ urlParams, location }: Props) {
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiTitle>
|
<EuiTitle>
|
||||||
<h1>
|
<h1>
|
||||||
{i18n.translate(
|
{i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', {
|
||||||
'xpack.apm.errorGroupDetails.errorGroupTitle',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Error group {errorGroupId}',
|
defaultMessage: 'Error group {errorGroupId}',
|
||||||
values: {
|
values: {
|
||||||
errorGroupId: getShortGroupId(urlParams.errorGroupId)
|
errorGroupId: getShortGroupId(urlParams.errorGroupId)
|
||||||
}
|
}
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
</h1>
|
</h1>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{isUnhandled && (
|
{isUnhandled && (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiBadge color="warning">
|
<EuiBadge color="warning">
|
||||||
{i18n.translate(
|
{i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', {
|
||||||
'xpack.apm.errorGroupDetails.unhandledLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Unhandled'
|
defaultMessage: 'Unhandled'
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
</EuiBadge>
|
</EuiBadge>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
)}
|
)}
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
<FilterBar />
|
<FilterBar />
|
||||||
|
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
|
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
|
@ -145,22 +151,17 @@ export function ErrorGroupDetailsView({ urlParams, location }: Props) {
|
||||||
</Label>
|
</Label>
|
||||||
<Message>{excMessage || NOT_AVAILABLE_LABEL}</Message>
|
<Message>{excMessage || NOT_AVAILABLE_LABEL}</Message>
|
||||||
<Label>
|
<Label>
|
||||||
{i18n.translate(
|
{i18n.translate('xpack.apm.errorGroupDetails.culpritLabel', {
|
||||||
'xpack.apm.errorGroupDetails.culpritLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Culprit'
|
defaultMessage: 'Culprit'
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
</Label>
|
</Label>
|
||||||
<Culprit>{culprit || NOT_AVAILABLE_LABEL}</Culprit>
|
<Culprit>{culprit || NOT_AVAILABLE_LABEL}</Culprit>
|
||||||
</EuiText>
|
</EuiText>
|
||||||
</Titles>
|
</Titles>
|
||||||
)}
|
)}
|
||||||
<ErrorDistributionRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<ErrorDistribution
|
<ErrorDistribution
|
||||||
distribution={data}
|
distribution={errorDistributionData}
|
||||||
title={i18n.translate(
|
title={i18n.translate(
|
||||||
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
|
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
|
||||||
{
|
{
|
||||||
|
@ -168,20 +169,15 @@ export function ErrorGroupDetailsView({ urlParams, location }: Props) {
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
{showDetails && (
|
{showDetails && (
|
||||||
<DetailView
|
<DetailView
|
||||||
errorGroup={errorGroup}
|
errorGroup={errorGroupData}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
location={location}
|
location={location}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
|
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
|
import createHistory from 'history/createHashHistory';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import {
|
// @ts-ignore
|
||||||
mockMoment,
|
import { createMockStore } from 'redux-test-utils';
|
||||||
mountWithRouterAndStore,
|
import { mockMoment, toJson } from '../../../../../utils/testHelpers';
|
||||||
toJson
|
|
||||||
} from '../../../../../utils/testHelpers';
|
|
||||||
import { ErrorGroupList } from '../index';
|
import { ErrorGroupList } from '../index';
|
||||||
import props from './props.json';
|
import props from './props.json';
|
||||||
|
|
||||||
|
@ -47,3 +47,30 @@ describe('ErrorGroupOverview -> List', () => {
|
||||||
expect(toJson(wrapper)).toMatchSnapshot();
|
expect(toJson(wrapper)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function mountWithRouterAndStore(
|
||||||
|
Component: React.ReactElement,
|
||||||
|
storeState = {}
|
||||||
|
) {
|
||||||
|
const store = createMockStore(storeState);
|
||||||
|
const history = createHistory();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
context: {
|
||||||
|
store,
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
route: {
|
||||||
|
match: { path: '/', url: '/', params: {}, isExact: true },
|
||||||
|
location: { pathname: '/', search: '', hash: '', key: '4yyjf5' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
childContextTypes: {
|
||||||
|
store: PropTypes.object.isRequired,
|
||||||
|
router: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return mount(Component, options);
|
||||||
|
}
|
||||||
|
|
|
@ -15,9 +15,12 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ErrorDistribution } from 'x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution';
|
import { ErrorDistribution } from 'x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution';
|
||||||
import { ErrorDistributionRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/errorDistribution';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
import { ErrorGroupOverviewRequest } from '../../../store/reactReduxRequest/errorGroupList';
|
import { useFetcher } from '../../../hooks/useFetcher';
|
||||||
|
import {
|
||||||
|
loadErrorDistribution,
|
||||||
|
loadErrorGroupList
|
||||||
|
} from '../../../services/rest/apm/error_groups';
|
||||||
import { ErrorGroupList } from './List';
|
import { ErrorGroupList } from './List';
|
||||||
|
|
||||||
interface ErrorGroupOverviewProps {
|
interface ErrorGroupOverviewProps {
|
||||||
|
@ -29,16 +32,45 @@ const ErrorGroupOverview: React.SFC<ErrorGroupOverviewProps> = ({
|
||||||
urlParams,
|
urlParams,
|
||||||
location
|
location
|
||||||
}) => {
|
}) => {
|
||||||
|
const {
|
||||||
|
serviceName,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
errorGroupId,
|
||||||
|
kuery,
|
||||||
|
sortField,
|
||||||
|
sortDirection
|
||||||
|
} = urlParams;
|
||||||
|
const { data: errorDistributionData } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadErrorDistribution({ serviceName, start, end, errorGroupId, kuery }),
|
||||||
|
[serviceName, start, end, errorGroupId, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: errorGroupListData } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadErrorGroupList({
|
||||||
|
serviceName,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
sortField,
|
||||||
|
sortDirection,
|
||||||
|
kuery
|
||||||
|
}),
|
||||||
|
[serviceName, start, end, sortField, sortDirection, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!errorDistributionData || !errorGroupListData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<EuiFlexGroup>
|
<EuiFlexGroup>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<ErrorDistributionRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<ErrorDistribution
|
<ErrorDistribution
|
||||||
distribution={data}
|
distribution={errorDistributionData}
|
||||||
title={i18n.translate(
|
title={i18n.translate(
|
||||||
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
|
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
|
||||||
{
|
{
|
||||||
|
@ -46,8 +78,6 @@ const ErrorGroupOverview: React.SFC<ErrorGroupOverviewProps> = ({
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
@ -59,16 +89,12 @@ const ErrorGroupOverview: React.SFC<ErrorGroupOverviewProps> = ({
|
||||||
<h3>Errors</h3>
|
<h3>Errors</h3>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
<ErrorGroupOverviewRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<ErrorGroupList
|
<ErrorGroupList
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
items={data}
|
items={errorGroupListData}
|
||||||
location={location}
|
location={location}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiDelayHide, EuiPortal, EuiProgress } from '@elastic/eui';
|
||||||
|
import React, { Fragment, useMemo, useReducer } from 'react';
|
||||||
|
|
||||||
|
export const GlobalFetchContext = React.createContext({
|
||||||
|
statuses: {},
|
||||||
|
dispatchStatus: (action: Action) => undefined as void
|
||||||
|
});
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
[key: string]: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Action {
|
||||||
|
isLoading: boolean;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reducer(statuses: State, action: Action) {
|
||||||
|
return { ...statuses, [action.name]: action.isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIsAnyLoading(statuses: State) {
|
||||||
|
return Object.values(statuses).some(isLoading => isLoading);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GlobalFetchIndicator({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [statuses, dispatchStatus] = useReducer(reducer, {});
|
||||||
|
const isLoading = useMemo(() => getIsAnyLoading(statuses), [statuses]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiDelayHide
|
||||||
|
hide={!isLoading}
|
||||||
|
minimumDuration={1000}
|
||||||
|
render={() => (
|
||||||
|
<EuiPortal>
|
||||||
|
<EuiProgress size="xs" position="fixed" />
|
||||||
|
</EuiPortal>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GlobalFetchContext.Provider
|
||||||
|
value={{ statuses, dispatchStatus }}
|
||||||
|
children={children}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,26 +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 { get, some } from 'lodash';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { IReduxState } from 'x-pack/plugins/apm/public/store/rootReducer';
|
|
||||||
import { STATUS } from '../../../../constants/index';
|
|
||||||
import { GlobalProgressView } from './view';
|
|
||||||
|
|
||||||
function getIsLoading(state: IReduxState) {
|
|
||||||
return some(
|
|
||||||
state.reactReduxRequest,
|
|
||||||
subState => get(subState, 'status') === STATUS.LOADING
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state = {} as IReduxState) {
|
|
||||||
return {
|
|
||||||
isLoading: getIsLoading(state)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GlobalProgress = connect(mapStateToProps)(GlobalProgressView);
|
|
|
@ -1,26 +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 { EuiDelayHide, EuiPortal, EuiProgress } from '@elastic/eui';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GlobalProgressView({ isLoading }: Props) {
|
|
||||||
return (
|
|
||||||
<EuiDelayHide
|
|
||||||
hide={!isLoading}
|
|
||||||
minimumDuration={1000}
|
|
||||||
render={() => (
|
|
||||||
<EuiPortal>
|
|
||||||
<EuiProgress size="xs" position="fixed" />
|
|
||||||
</EuiPortal>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,21 +4,31 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { STATUS } from '../../../../constants/index';
|
import {
|
||||||
import { LicenceRequest } from '../../../../store/reactReduxRequest/license';
|
FETCH_STATUS,
|
||||||
|
useFetcher
|
||||||
|
} from 'x-pack/plugins/apm/public/hooks/useFetcher';
|
||||||
|
import { loadLicense } from 'x-pack/plugins/apm/public/services/rest/xpack';
|
||||||
import { InvalidLicenseNotification } from './InvalidLicenseNotification';
|
import { InvalidLicenseNotification } from './InvalidLicenseNotification';
|
||||||
|
|
||||||
export const LicenseCheck: React.FunctionComponent = ({ children }) => {
|
const initialLicense = {
|
||||||
return (
|
features: {
|
||||||
<LicenceRequest
|
watcher: { is_available: false },
|
||||||
render={({ data: licenseData, status: licenseStatus }) => {
|
ml: { is_available: false }
|
||||||
const hasValidLicense = licenseData.license.is_active;
|
},
|
||||||
if (licenseStatus === STATUS.SUCCESS && !hasValidLicense) {
|
license: { is_active: false }
|
||||||
|
};
|
||||||
|
export const LicenseContext = React.createContext(initialLicense);
|
||||||
|
|
||||||
|
export const LicenseCheck: React.FC = ({ children }) => {
|
||||||
|
const { data = initialLicense, status } = useFetcher(() => loadLicense(), []);
|
||||||
|
const hasValidLicense = data.license.is_active;
|
||||||
|
|
||||||
|
// if license is invalid show an error message
|
||||||
|
if (status === FETCH_STATUS.SUCCESS && !hasValidLicense) {
|
||||||
return <InvalidLicenseNotification />;
|
return <InvalidLicenseNotification />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
// render rest of application and pass down license via context
|
||||||
}}
|
return <LicenseContext.Provider value={data} children={children} />;
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ import styled from 'styled-components';
|
||||||
import { px, topNavHeight, unit, units } from '../../../style/variables';
|
import { px, topNavHeight, unit, units } from '../../../style/variables';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import ConnectRouterToRedux from '../../shared/ConnectRouterToRedux';
|
import ConnectRouterToRedux from '../../shared/ConnectRouterToRedux';
|
||||||
|
import { GlobalFetchIndicator } from './GlobalFetchIndicator';
|
||||||
import { LicenseCheck } from './LicenseCheck';
|
import { LicenseCheck } from './LicenseCheck';
|
||||||
import { routes } from './routeConfig';
|
import { routes } from './routeConfig';
|
||||||
import { ScrollToTopOnPathChange } from './ScrollToTopOnPathChange';
|
import { ScrollToTopOnPathChange } from './ScrollToTopOnPathChange';
|
||||||
|
@ -23,6 +24,7 @@ const MainContainer = styled.div`
|
||||||
|
|
||||||
export function Main() {
|
export function Main() {
|
||||||
return (
|
return (
|
||||||
|
<GlobalFetchIndicator>
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<UpdateBreadcrumbs />
|
<UpdateBreadcrumbs />
|
||||||
<Route component={ConnectRouterToRedux} />
|
<Route component={ConnectRouterToRedux} />
|
||||||
|
@ -35,5 +37,6 @@ export function Main() {
|
||||||
</Switch>
|
</Switch>
|
||||||
</LicenseCheck>
|
</LicenseCheck>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
|
</GlobalFetchIndicator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,11 @@ import React from 'react';
|
||||||
import CustomPlot from 'x-pack/plugins/apm/public/components/shared/charts/CustomPlot';
|
import CustomPlot from 'x-pack/plugins/apm/public/components/shared/charts/CustomPlot';
|
||||||
import { HoverXHandlers } from 'x-pack/plugins/apm/public/components/shared/charts/SyncChartGroup';
|
import { HoverXHandlers } from 'x-pack/plugins/apm/public/components/shared/charts/SyncChartGroup';
|
||||||
import { asPercent } from 'x-pack/plugins/apm/public/utils/formatters';
|
import { asPercent } from 'x-pack/plugins/apm/public/utils/formatters';
|
||||||
import { CPUChartAPIResponse } from 'x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/transformer';
|
|
||||||
import { Coordinate } from 'x-pack/plugins/apm/typings/timeseries';
|
import { Coordinate } from 'x-pack/plugins/apm/typings/timeseries';
|
||||||
|
import { CPUMetricSeries } from '../../../store/selectors/chartSelectors';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: CPUChartAPIResponse;
|
data: CPUMetricSeries;
|
||||||
hoverXHandlers: HoverXHandlers;
|
hoverXHandlers: HoverXHandlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,11 @@ import React from 'react';
|
||||||
import CustomPlot from 'x-pack/plugins/apm/public/components/shared/charts/CustomPlot';
|
import CustomPlot from 'x-pack/plugins/apm/public/components/shared/charts/CustomPlot';
|
||||||
import { HoverXHandlers } from 'x-pack/plugins/apm/public/components/shared/charts/SyncChartGroup';
|
import { HoverXHandlers } from 'x-pack/plugins/apm/public/components/shared/charts/SyncChartGroup';
|
||||||
import { asPercent } from 'x-pack/plugins/apm/public/utils/formatters';
|
import { asPercent } from 'x-pack/plugins/apm/public/utils/formatters';
|
||||||
import { MemoryChartAPIResponse } from 'x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/transformer';
|
|
||||||
import { Coordinate } from 'x-pack/plugins/apm/typings/timeseries';
|
import { Coordinate } from 'x-pack/plugins/apm/typings/timeseries';
|
||||||
|
import { MemoryMetricSeries } from '../../../store/selectors/chartSelectors';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: MemoryChartAPIResponse;
|
data: MemoryMetricSeries;
|
||||||
hoverXHandlers: HoverXHandlers;
|
hoverXHandlers: HoverXHandlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,391 +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 {
|
|
||||||
EuiButton,
|
|
||||||
EuiCallOut,
|
|
||||||
EuiFlexGroup,
|
|
||||||
EuiFlexItem,
|
|
||||||
EuiFlyout,
|
|
||||||
EuiFlyoutBody,
|
|
||||||
EuiFlyoutFooter,
|
|
||||||
EuiFlyoutHeader,
|
|
||||||
EuiFormRow,
|
|
||||||
EuiSpacer,
|
|
||||||
EuiText,
|
|
||||||
EuiTitle
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import { Location } from 'history';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { toastNotifications } from 'ui/notify';
|
|
||||||
import { getMlJobId } from 'x-pack/plugins/apm/common/ml_job_constants';
|
|
||||||
import { KibanaLink } from 'x-pack/plugins/apm/public/components/shared/Links/KibanaLink';
|
|
||||||
import { MLJobLink } from 'x-pack/plugins/apm/public/components/shared/Links/MLJobLink';
|
|
||||||
import { startMLJob } from 'x-pack/plugins/apm/public/services/rest/ml';
|
|
||||||
import { getAPMIndexPattern } from 'x-pack/plugins/apm/public/services/rest/savedObjects';
|
|
||||||
import { MLJobsRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/machineLearningJobs';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
|
||||||
import { TransactionSelect } from './TransactionSelect';
|
|
||||||
|
|
||||||
interface FlyoutProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
location: Location;
|
|
||||||
serviceTransactionTypes: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FlyoutState {
|
|
||||||
isLoading: boolean;
|
|
||||||
hasMLJob: boolean;
|
|
||||||
hasIndexPattern: boolean;
|
|
||||||
selectedTransactionType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|
||||||
public state = {
|
|
||||||
isLoading: false,
|
|
||||||
hasIndexPattern: false,
|
|
||||||
hasMLJob: false,
|
|
||||||
selectedTransactionType: this.props.urlParams.transactionType
|
|
||||||
};
|
|
||||||
public willUnmount = false;
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
this.willUnmount = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async componentDidMount() {
|
|
||||||
const indexPattern = await getAPMIndexPattern();
|
|
||||||
if (!this.willUnmount) {
|
|
||||||
this.setState({ hasIndexPattern: !!indexPattern });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This should use `getDerivedStateFromProps`
|
|
||||||
public componentDidUpdate(prevProps: FlyoutProps) {
|
|
||||||
if (
|
|
||||||
prevProps.urlParams.transactionType !==
|
|
||||||
this.props.urlParams.transactionType
|
|
||||||
) {
|
|
||||||
this.setState({
|
|
||||||
selectedTransactionType: this.props.urlParams.transactionType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public createJob = async () => {
|
|
||||||
this.setState({ isLoading: true });
|
|
||||||
try {
|
|
||||||
const { serviceName, transactionType } = this.props.urlParams;
|
|
||||||
if (!serviceName || !transactionType) {
|
|
||||||
throw new Error(
|
|
||||||
'Service name and transaction type are required to create this ML job'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const res = await startMLJob({ serviceName, transactionType });
|
|
||||||
const didSucceed = res.datafeeds[0].success && res.jobs[0].success;
|
|
||||||
if (!didSucceed) {
|
|
||||||
throw new Error('Creating ML job failed');
|
|
||||||
}
|
|
||||||
this.addSuccessToast();
|
|
||||||
} catch (e) {
|
|
||||||
this.addErrorToast();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
this.props.onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
public addErrorToast = () => {
|
|
||||||
const { urlParams } = this.props;
|
|
||||||
const { serviceName } = urlParams;
|
|
||||||
|
|
||||||
if (!serviceName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toastNotifications.addWarning({
|
|
||||||
title: i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Job creation failed'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
text: (
|
|
||||||
<p>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'Your current license may not allow for creating machine learning jobs, or this job may already exist.'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public addSuccessToast = () => {
|
|
||||||
const { location, urlParams } = this.props;
|
|
||||||
const { serviceName, transactionType } = urlParams;
|
|
||||||
|
|
||||||
if (!serviceName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toastNotifications.addSuccess({
|
|
||||||
title: i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Job successfully created'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
text: (
|
|
||||||
<p>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'The analysis is now running for {serviceName} ({transactionType}). It might take a while before results are added to the response times graph.',
|
|
||||||
values: {
|
|
||||||
serviceName,
|
|
||||||
transactionType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)}{' '}
|
|
||||||
<MLJobLink
|
|
||||||
serviceName={serviceName}
|
|
||||||
transactionType={transactionType}
|
|
||||||
location={location}
|
|
||||||
>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
|
|
||||||
{
|
|
||||||
defaultMessage: 'View job'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</MLJobLink>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const { isOpen, onClose, urlParams, location } = this.props;
|
|
||||||
const { serviceName, transactionType } = urlParams;
|
|
||||||
const { isLoading, hasIndexPattern, selectedTransactionType } = this.state;
|
|
||||||
|
|
||||||
if (!isOpen || !serviceName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MLJobsRequest
|
|
||||||
serviceName={serviceName}
|
|
||||||
render={({ data, status }) => {
|
|
||||||
if (status === 'LOADING') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMLJob = data.jobs.some(
|
|
||||||
job =>
|
|
||||||
job.job_id === getMlJobId(serviceName, selectedTransactionType)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiFlyout onClose={onClose} size="s">
|
|
||||||
<EuiFlyoutHeader>
|
|
||||||
<EuiTitle>
|
|
||||||
<h2>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Enable anomaly detection'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
</EuiTitle>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
</EuiFlyoutHeader>
|
|
||||||
<EuiFlyoutBody>
|
|
||||||
{hasMLJob && (
|
|
||||||
<div>
|
|
||||||
<EuiCallOut
|
|
||||||
title={i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Job already exists'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
color="success"
|
|
||||||
iconType="check"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'There is currently a job running for {serviceName} ({transactionType}).',
|
|
||||||
values: {
|
|
||||||
serviceName,
|
|
||||||
transactionType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)}{' '}
|
|
||||||
<MLJobLink
|
|
||||||
serviceName={serviceName}
|
|
||||||
transactionType={transactionType}
|
|
||||||
location={location}
|
|
||||||
>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText',
|
|
||||||
{
|
|
||||||
defaultMessage: 'View existing job'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</MLJobLink>
|
|
||||||
</p>
|
|
||||||
</EuiCallOut>
|
|
||||||
<EuiSpacer size="m" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!hasIndexPattern && (
|
|
||||||
<div>
|
|
||||||
<EuiCallOut
|
|
||||||
title={
|
|
||||||
<span>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle"
|
|
||||||
defaultMessage="No APM index pattern available. To create a job, please import the APM index pattern via the {setupInstructionLink}"
|
|
||||||
values={{
|
|
||||||
setupInstructionLink: (
|
|
||||||
<KibanaLink
|
|
||||||
pathname={'/app/kibana'}
|
|
||||||
hash={`/home/tutorial/apm`}
|
|
||||||
>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle.setupInstructionLinkText',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Setup Instructions'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</KibanaLink>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
color="warning"
|
|
||||||
iconType="alert"
|
|
||||||
/>
|
|
||||||
<EuiSpacer size="m" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<EuiText>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription"
|
|
||||||
defaultMessage="Here you can create a machine learning job to calculate anomaly scores on durations for APM transactions
|
|
||||||
within the {serviceName} service. Once enabled, {transactionDurationGraphText} will show the expected bounds and annotate
|
|
||||||
the graph once the anomaly score is >=75."
|
|
||||||
values={{
|
|
||||||
serviceName,
|
|
||||||
transactionDurationGraphText: (
|
|
||||||
<b>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText',
|
|
||||||
{
|
|
||||||
defaultMessage: 'the transaction duration graph'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</b>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription"
|
|
||||||
defaultMessage="Jobs can be created for each service + transaction type combination.
|
|
||||||
Once a job is created, you can manage it and see more details in the {mlJobsPageLink}."
|
|
||||||
values={{
|
|
||||||
mlJobsPageLink: (
|
|
||||||
<KibanaLink pathname={'/app/ml'}>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'Machine Learning jobs management page'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</KibanaLink>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>{' '}
|
|
||||||
<em>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'Note: It might take a few minutes for the job to begin calculating results.'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</em>
|
|
||||||
</p>
|
|
||||||
</EuiText>
|
|
||||||
|
|
||||||
<EuiSpacer />
|
|
||||||
</EuiFlyoutBody>
|
|
||||||
<EuiFlyoutFooter>
|
|
||||||
<EuiFlexGroup
|
|
||||||
justifyContent="spaceBetween"
|
|
||||||
alignItems="flexEnd"
|
|
||||||
>
|
|
||||||
<EuiFlexItem>
|
|
||||||
{this.props.serviceTransactionTypes.length > 1 ? (
|
|
||||||
<TransactionSelect
|
|
||||||
serviceName={serviceName}
|
|
||||||
transactionTypes={this.props.serviceTransactionTypes}
|
|
||||||
selected={this.state.selectedTransactionType}
|
|
||||||
existingJobs={data.jobs}
|
|
||||||
onChange={value =>
|
|
||||||
this.setState({
|
|
||||||
selectedTransactionType: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiFormRow>
|
|
||||||
<EuiButton
|
|
||||||
onClick={this.createJob}
|
|
||||||
fill
|
|
||||||
disabled={isLoading || hasMLJob || !hasIndexPattern}
|
|
||||||
>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Create new job'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</EuiButton>
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
</EuiFlyoutFooter>
|
|
||||||
</EuiFlyout>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,203 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { Location } from 'history';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { toastNotifications } from 'ui/notify';
|
||||||
|
import { MLJobLink } from 'x-pack/plugins/apm/public/components/shared/Links/MLJobLink';
|
||||||
|
import { startMLJob } from 'x-pack/plugins/apm/public/services/rest/ml';
|
||||||
|
import { getAPMIndexPattern } from 'x-pack/plugins/apm/public/services/rest/savedObjects';
|
||||||
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { MachineLearningFlyoutView } from './view';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
location: Location;
|
||||||
|
serviceTransactionTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isCreatingJob: boolean;
|
||||||
|
hasMLJob: boolean;
|
||||||
|
hasIndexPattern: boolean;
|
||||||
|
selectedTransactionType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MachineLearningFlyout extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
isCreatingJob: false,
|
||||||
|
hasIndexPattern: false,
|
||||||
|
hasMLJob: false,
|
||||||
|
selectedTransactionType: this.props.urlParams.transactionType
|
||||||
|
};
|
||||||
|
public willUnmount = false;
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.willUnmount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
const indexPattern = await getAPMIndexPattern();
|
||||||
|
if (!this.willUnmount) {
|
||||||
|
// TODO: this is causing warning from react because setState happens after
|
||||||
|
// the component has been unmounted - dispite of the checks
|
||||||
|
this.setState({ hasIndexPattern: !!indexPattern });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This should use `getDerivedStateFromProps`
|
||||||
|
public componentDidUpdate(prevProps: Props) {
|
||||||
|
if (
|
||||||
|
prevProps.urlParams.transactionType !==
|
||||||
|
this.props.urlParams.transactionType
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
selectedTransactionType: this.props.urlParams.transactionType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onClickCreate = async () => {
|
||||||
|
this.setState({ isCreatingJob: true });
|
||||||
|
try {
|
||||||
|
const { serviceName, transactionType } = this.props.urlParams;
|
||||||
|
if (!serviceName || !transactionType) {
|
||||||
|
throw new Error(
|
||||||
|
'Service name and transaction type are required to create this ML job'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const res = await startMLJob({ serviceName, transactionType });
|
||||||
|
const didSucceed = res.datafeeds[0].success && res.jobs[0].success;
|
||||||
|
if (!didSucceed) {
|
||||||
|
throw new Error('Creating ML job failed');
|
||||||
|
}
|
||||||
|
this.addSuccessToast();
|
||||||
|
} catch (e) {
|
||||||
|
this.addErrorToast();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ isCreatingJob: false });
|
||||||
|
this.props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
public addErrorToast = () => {
|
||||||
|
const { urlParams } = this.props;
|
||||||
|
const { serviceName } = urlParams;
|
||||||
|
|
||||||
|
if (!serviceName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toastNotifications.addWarning({
|
||||||
|
title: i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Job creation failed'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
text: (
|
||||||
|
<p>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'Your current license may not allow for creating machine learning jobs, or this job may already exist.'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public addSuccessToast = () => {
|
||||||
|
const { location, urlParams } = this.props;
|
||||||
|
const { serviceName, transactionType } = urlParams;
|
||||||
|
|
||||||
|
if (!serviceName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toastNotifications.addSuccess({
|
||||||
|
title: i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Job successfully created'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
text: (
|
||||||
|
<p>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'The analysis is now running for {serviceName} ({transactionType}). It might take a while before results are added to the response times graph.',
|
||||||
|
values: {
|
||||||
|
serviceName,
|
||||||
|
transactionType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)}{' '}
|
||||||
|
<MLJobLink
|
||||||
|
serviceName={serviceName}
|
||||||
|
transactionType={transactionType}
|
||||||
|
location={location}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
|
||||||
|
{
|
||||||
|
defaultMessage: 'View job'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</MLJobLink>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public onChangeTransaction(value: string) {
|
||||||
|
this.setState({
|
||||||
|
selectedTransactionType: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
urlParams,
|
||||||
|
location,
|
||||||
|
serviceTransactionTypes
|
||||||
|
} = this.props;
|
||||||
|
const { serviceName, transactionType } = urlParams;
|
||||||
|
const {
|
||||||
|
isCreatingJob,
|
||||||
|
hasIndexPattern,
|
||||||
|
selectedTransactionType
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!isOpen || !serviceName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MachineLearningFlyoutView
|
||||||
|
hasIndexPattern={hasIndexPattern}
|
||||||
|
isCreatingJob={isCreatingJob}
|
||||||
|
location={location}
|
||||||
|
onChangeTransaction={this.onChangeTransaction}
|
||||||
|
onClickCreate={this.onClickCreate}
|
||||||
|
onClose={onClose}
|
||||||
|
selectedTransactionType={selectedTransactionType}
|
||||||
|
serviceName={serviceName}
|
||||||
|
serviceTransactionTypes={serviceTransactionTypes}
|
||||||
|
transactionType={transactionType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
EuiButton,
|
||||||
|
EuiCallOut,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiFlyout,
|
||||||
|
EuiFlyoutBody,
|
||||||
|
EuiFlyoutFooter,
|
||||||
|
EuiFlyoutHeader,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiText,
|
||||||
|
EuiTitle
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { Location } from 'history';
|
||||||
|
import React from 'react';
|
||||||
|
import { getMlJobId } from 'x-pack/plugins/apm/common/ml_job_constants';
|
||||||
|
import { KibanaLink } from 'x-pack/plugins/apm/public/components/shared/Links/KibanaLink';
|
||||||
|
import { MLJobLink } from 'x-pack/plugins/apm/public/components/shared/Links/MLJobLink';
|
||||||
|
import {
|
||||||
|
FETCH_STATUS,
|
||||||
|
useFetcher
|
||||||
|
} from 'x-pack/plugins/apm/public/hooks/useFetcher';
|
||||||
|
import { getMLJob } from 'x-pack/plugins/apm/public/services/rest/ml';
|
||||||
|
import { TransactionSelect } from './TransactionSelect';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hasIndexPattern: boolean;
|
||||||
|
isCreatingJob: boolean;
|
||||||
|
location: Location;
|
||||||
|
onChangeTransaction: (value: string) => void;
|
||||||
|
onClickCreate: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
selectedTransactionType?: string;
|
||||||
|
serviceName: string;
|
||||||
|
serviceTransactionTypes: string[];
|
||||||
|
transactionType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_DATA = { count: 0, jobs: [] };
|
||||||
|
export function MachineLearningFlyoutView({
|
||||||
|
hasIndexPattern,
|
||||||
|
isCreatingJob,
|
||||||
|
location,
|
||||||
|
onChangeTransaction,
|
||||||
|
onClickCreate,
|
||||||
|
onClose,
|
||||||
|
selectedTransactionType,
|
||||||
|
serviceName,
|
||||||
|
serviceTransactionTypes,
|
||||||
|
transactionType
|
||||||
|
}: Props) {
|
||||||
|
const { data = INITIAL_DATA, status } = useFetcher(
|
||||||
|
() => getMLJob({ serviceName, transactionType }),
|
||||||
|
[serviceName, transactionType]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === FETCH_STATUS.LOADING) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMLJob = data.jobs.some(
|
||||||
|
job => job.job_id === getMlJobId(serviceName, selectedTransactionType)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlyout onClose={onClose} size="s">
|
||||||
|
<EuiFlyoutHeader>
|
||||||
|
<EuiTitle>
|
||||||
|
<h2>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Enable anomaly detection'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
</EuiFlyoutHeader>
|
||||||
|
<EuiFlyoutBody>
|
||||||
|
{hasMLJob && (
|
||||||
|
<div>
|
||||||
|
<EuiCallOut
|
||||||
|
title={i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Job already exists'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
color="success"
|
||||||
|
iconType="check"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'There is currently a job running for {serviceName} ({transactionType}).',
|
||||||
|
values: {
|
||||||
|
serviceName,
|
||||||
|
transactionType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)}{' '}
|
||||||
|
<MLJobLink
|
||||||
|
serviceName={serviceName}
|
||||||
|
transactionType={transactionType}
|
||||||
|
location={location}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText',
|
||||||
|
{
|
||||||
|
defaultMessage: 'View existing job'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</MLJobLink>
|
||||||
|
</p>
|
||||||
|
</EuiCallOut>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasIndexPattern && (
|
||||||
|
<div>
|
||||||
|
<EuiCallOut
|
||||||
|
title={
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle"
|
||||||
|
defaultMessage="No APM index pattern available. To create a job, please import the APM index pattern via the {setupInstructionLink}"
|
||||||
|
values={{
|
||||||
|
setupInstructionLink: (
|
||||||
|
<KibanaLink
|
||||||
|
pathname={'/app/kibana'}
|
||||||
|
hash={`/home/tutorial/apm`}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle.setupInstructionLinkText',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Setup Instructions'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</KibanaLink>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
color="warning"
|
||||||
|
iconType="alert"
|
||||||
|
/>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<EuiText>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription"
|
||||||
|
defaultMessage="Here you can create a machine learning job to calculate anomaly scores on durations for APM transactions
|
||||||
|
within the {serviceName} service. Once enabled, {transactionDurationGraphText} will show the expected bounds and annotate
|
||||||
|
the graph once the anomaly score is >=75."
|
||||||
|
values={{
|
||||||
|
serviceName,
|
||||||
|
transactionDurationGraphText: (
|
||||||
|
<b>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText',
|
||||||
|
{
|
||||||
|
defaultMessage: 'the transaction duration graph'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</b>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription"
|
||||||
|
defaultMessage="Jobs can be created for each service + transaction type combination.
|
||||||
|
Once a job is created, you can manage it and see more details in the {mlJobsPageLink}."
|
||||||
|
values={{
|
||||||
|
mlJobsPageLink: (
|
||||||
|
<KibanaLink pathname={'/app/ml'}>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Machine Learning jobs management page'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</KibanaLink>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
<em>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'Note: It might take a few minutes for the job to begin calculating results.'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
</EuiText>
|
||||||
|
|
||||||
|
<EuiSpacer />
|
||||||
|
</EuiFlyoutBody>
|
||||||
|
<EuiFlyoutFooter>
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
|
||||||
|
<EuiFlexItem>
|
||||||
|
{serviceTransactionTypes.length > 1 ? (
|
||||||
|
<TransactionSelect
|
||||||
|
serviceName={serviceName}
|
||||||
|
transactionTypes={serviceTransactionTypes}
|
||||||
|
selected={selectedTransactionType}
|
||||||
|
existingJobs={data.jobs}
|
||||||
|
onChange={onChangeTransaction}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiFormRow>
|
||||||
|
<EuiButton
|
||||||
|
onClick={onClickCreate}
|
||||||
|
fill
|
||||||
|
disabled={isCreatingJob || hasMLJob || !hasIndexPattern}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Create new job'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlyoutFooter>
|
||||||
|
</EuiFlyout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,16 +4,181 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import {
|
||||||
import { IReduxState } from 'x-pack/plugins/apm/public/store/rootReducer';
|
EuiButton,
|
||||||
import { selectIsMLAvailable } from 'x-pack/plugins/apm/public/store/selectors/license';
|
EuiContextMenu,
|
||||||
import { ServiceIntegrationsView } from './view';
|
EuiContextMenuPanelItemDescriptor,
|
||||||
|
EuiPopover
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { Location } from 'history';
|
||||||
|
import { memoize } from 'lodash';
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import chrome from 'ui/chrome';
|
||||||
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { LicenseContext } from '../../Main/LicenseCheck';
|
||||||
|
import { MachineLearningFlyout } from './MachineLearningFlyout';
|
||||||
|
import { WatcherFlyout } from './WatcherFlyout';
|
||||||
|
|
||||||
function mapStateToProps(state = {} as IReduxState) {
|
interface Props {
|
||||||
return {
|
location: Location;
|
||||||
mlAvailable: selectIsMLAvailable(state)
|
transactionTypes: string[];
|
||||||
};
|
urlParams: IUrlParams;
|
||||||
}
|
}
|
||||||
|
interface State {
|
||||||
|
isPopoverOpen: boolean;
|
||||||
|
activeFlyout: FlyoutName;
|
||||||
|
}
|
||||||
|
type FlyoutName = null | 'ML' | 'Watcher';
|
||||||
|
|
||||||
const ServiceIntegrations = connect(mapStateToProps)(ServiceIntegrationsView);
|
export class ServiceIntegrations extends React.Component<Props, State> {
|
||||||
export { ServiceIntegrations };
|
public state: State = { isPopoverOpen: false, activeFlyout: null };
|
||||||
|
|
||||||
|
public getPanelItems = memoize((mlAvailable: boolean) => {
|
||||||
|
let panelItems: EuiContextMenuPanelItemDescriptor[] = [];
|
||||||
|
if (mlAvailable) {
|
||||||
|
panelItems = panelItems.concat(this.getMLPanelItems());
|
||||||
|
}
|
||||||
|
return panelItems.concat(this.getWatcherPanelItems());
|
||||||
|
});
|
||||||
|
|
||||||
|
public getMLPanelItems = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Enable ML anomaly detection'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
icon: 'machineLearningApp',
|
||||||
|
toolTipContent: i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Set up a machine learning job for this service'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
this.closePopover();
|
||||||
|
this.openFlyout('ML');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.integrationsMenu.viewMLJobsButtonLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'View existing ML jobs'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
icon: 'machineLearningApp',
|
||||||
|
href: chrome.addBasePath('/app/ml'),
|
||||||
|
target: '_blank',
|
||||||
|
onClick: () => this.closePopover()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
public getWatcherPanelItems = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Enable watcher error reports'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
icon: 'watchesApp',
|
||||||
|
onClick: () => {
|
||||||
|
this.closePopover();
|
||||||
|
this.openFlyout('Watcher');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'View existing watches'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
icon: 'watchesApp',
|
||||||
|
href: chrome.addBasePath(
|
||||||
|
'/app/kibana#/management/elasticsearch/watcher'
|
||||||
|
),
|
||||||
|
target: '_blank',
|
||||||
|
onClick: () => this.closePopover()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
public openPopover = () =>
|
||||||
|
this.setState({
|
||||||
|
isPopoverOpen: true
|
||||||
|
});
|
||||||
|
|
||||||
|
public closePopover = () =>
|
||||||
|
this.setState({
|
||||||
|
isPopoverOpen: false
|
||||||
|
});
|
||||||
|
|
||||||
|
public openFlyout = (name: FlyoutName) =>
|
||||||
|
this.setState({ activeFlyout: name });
|
||||||
|
|
||||||
|
public closeFlyouts = () => this.setState({ activeFlyout: null });
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const button = (
|
||||||
|
<EuiButton
|
||||||
|
iconType="arrowDown"
|
||||||
|
iconSide="right"
|
||||||
|
onClick={this.openPopover}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Integrations'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</EuiButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LicenseContext.Consumer>
|
||||||
|
{license => (
|
||||||
|
<Fragment>
|
||||||
|
<EuiPopover
|
||||||
|
id="integrations-menu"
|
||||||
|
button={button}
|
||||||
|
isOpen={this.state.isPopoverOpen}
|
||||||
|
closePopover={this.closePopover}
|
||||||
|
panelPaddingSize="none"
|
||||||
|
anchorPosition="downRight"
|
||||||
|
>
|
||||||
|
<EuiContextMenu
|
||||||
|
initialPanelId={0}
|
||||||
|
panels={[
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
items: this.getPanelItems(license.features.ml.is_available)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</EuiPopover>
|
||||||
|
<MachineLearningFlyout
|
||||||
|
location={this.props.location}
|
||||||
|
isOpen={this.state.activeFlyout === 'ML'}
|
||||||
|
onClose={this.closeFlyouts}
|
||||||
|
urlParams={this.props.urlParams}
|
||||||
|
serviceTransactionTypes={this.props.transactionTypes}
|
||||||
|
/>
|
||||||
|
<WatcherFlyout
|
||||||
|
location={this.props.location}
|
||||||
|
isOpen={this.state.activeFlyout === 'Watcher'}
|
||||||
|
onClose={this.closeFlyouts}
|
||||||
|
urlParams={this.props.urlParams}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</LicenseContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,183 +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 {
|
|
||||||
EuiButton,
|
|
||||||
EuiContextMenu,
|
|
||||||
EuiContextMenuPanelItemDescriptor,
|
|
||||||
EuiPopover
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { Location } from 'history';
|
|
||||||
import { memoize } from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import chrome from 'ui/chrome';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
|
||||||
import { MachineLearningFlyout } from './MachineLearningFlyout';
|
|
||||||
import { WatcherFlyout } from './WatcherFlyout';
|
|
||||||
|
|
||||||
export interface ServiceIntegrationProps {
|
|
||||||
mlAvailable: boolean;
|
|
||||||
location: Location;
|
|
||||||
transactionTypes: string[];
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
}
|
|
||||||
interface ServiceIntegrationState {
|
|
||||||
isPopoverOpen: boolean;
|
|
||||||
activeFlyout: FlyoutName;
|
|
||||||
}
|
|
||||||
type FlyoutName = null | 'ML' | 'Watcher';
|
|
||||||
|
|
||||||
export class ServiceIntegrationsView extends React.Component<
|
|
||||||
ServiceIntegrationProps,
|
|
||||||
ServiceIntegrationState
|
|
||||||
> {
|
|
||||||
public state = { isPopoverOpen: false, activeFlyout: null };
|
|
||||||
|
|
||||||
public getPanelItems = memoize((mlAvailable: boolean) => {
|
|
||||||
let panelItems: EuiContextMenuPanelItemDescriptor[] = [];
|
|
||||||
if (mlAvailable) {
|
|
||||||
panelItems = panelItems.concat(this.getMLPanelItems());
|
|
||||||
}
|
|
||||||
return panelItems.concat(this.getWatcherPanelItems());
|
|
||||||
});
|
|
||||||
|
|
||||||
public getMLPanelItems = () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Enable ML anomaly detection'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
icon: 'machineLearningApp',
|
|
||||||
toolTipContent: i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Set up a machine learning job for this service'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
onClick: () => {
|
|
||||||
this.closePopover();
|
|
||||||
this.openFlyout('ML');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.integrationsMenu.viewMLJobsButtonLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'View existing ML jobs'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
icon: 'machineLearningApp',
|
|
||||||
href: chrome.addBasePath('/app/ml'),
|
|
||||||
target: '_blank',
|
|
||||||
onClick: () => this.closePopover()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
public getWatcherPanelItems = () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Enable watcher error reports'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
icon: 'watchesApp',
|
|
||||||
onClick: () => {
|
|
||||||
this.closePopover();
|
|
||||||
this.openFlyout('Watcher');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'View existing watches'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
icon: 'watchesApp',
|
|
||||||
href: chrome.addBasePath(
|
|
||||||
'/app/kibana#/management/elasticsearch/watcher'
|
|
||||||
),
|
|
||||||
target: '_blank',
|
|
||||||
onClick: () => this.closePopover()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
public openPopover = () =>
|
|
||||||
this.setState({
|
|
||||||
isPopoverOpen: true
|
|
||||||
});
|
|
||||||
|
|
||||||
public closePopover = () =>
|
|
||||||
this.setState({
|
|
||||||
isPopoverOpen: false
|
|
||||||
});
|
|
||||||
|
|
||||||
public openFlyout = (name: FlyoutName) =>
|
|
||||||
this.setState({ activeFlyout: name });
|
|
||||||
|
|
||||||
public closeFlyouts = () => this.setState({ activeFlyout: null });
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const button = (
|
|
||||||
<EuiButton
|
|
||||||
iconType="arrowDown"
|
|
||||||
iconSide="right"
|
|
||||||
onClick={this.openPopover}
|
|
||||||
>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Integrations'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</EuiButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<EuiPopover
|
|
||||||
id="integrations-menu"
|
|
||||||
button={button}
|
|
||||||
isOpen={this.state.isPopoverOpen}
|
|
||||||
closePopover={this.closePopover}
|
|
||||||
panelPaddingSize="none"
|
|
||||||
anchorPosition="downRight"
|
|
||||||
>
|
|
||||||
<EuiContextMenu
|
|
||||||
initialPanelId={0}
|
|
||||||
panels={[
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
items: this.getPanelItems(this.props.mlAvailable)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</EuiPopover>
|
|
||||||
<MachineLearningFlyout
|
|
||||||
location={this.props.location}
|
|
||||||
isOpen={this.state.activeFlyout === 'ML'}
|
|
||||||
onClose={this.closeFlyouts}
|
|
||||||
urlParams={this.props.urlParams}
|
|
||||||
serviceTransactionTypes={this.props.transactionTypes}
|
|
||||||
/>
|
|
||||||
<WatcherFlyout
|
|
||||||
location={this.props.location}
|
|
||||||
isOpen={this.state.activeFlyout === 'Watcher'}
|
|
||||||
onClose={this.closeFlyouts}
|
|
||||||
urlParams={this.props.urlParams}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,10 +17,11 @@ import React from 'react';
|
||||||
import { ErrorDistribution } from 'x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution';
|
import { ErrorDistribution } from 'x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution';
|
||||||
import { SyncChartGroup } from 'x-pack/plugins/apm/public/components/shared/charts/SyncChartGroup';
|
import { SyncChartGroup } from 'x-pack/plugins/apm/public/components/shared/charts/SyncChartGroup';
|
||||||
import { TransactionCharts } from 'x-pack/plugins/apm/public/components/shared/charts/TransactionCharts';
|
import { TransactionCharts } from 'x-pack/plugins/apm/public/components/shared/charts/TransactionCharts';
|
||||||
import { ErrorDistributionRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/errorDistribution';
|
|
||||||
import { MetricsChartDataRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/serviceMetricsCharts';
|
|
||||||
import { TransactionOverviewChartsRequestForAllTypes } from 'x-pack/plugins/apm/public/store/reactReduxRequest/transactionOverviewCharts';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { useFetcher } from '../../../hooks/useFetcher';
|
||||||
|
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
|
||||||
|
import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts';
|
||||||
|
import { loadErrorDistribution } from '../../../services/rest/apm/error_groups';
|
||||||
import { CPUUsageChart } from './CPUUsageChart';
|
import { CPUUsageChart } from './CPUUsageChart';
|
||||||
import { MemoryUsageChart } from './MemoryUsageChart';
|
import { MemoryUsageChart } from './MemoryUsageChart';
|
||||||
|
|
||||||
|
@ -30,29 +31,38 @@ interface ServiceMetricsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceMetrics({ urlParams, location }: ServiceMetricsProps) {
|
export function ServiceMetrics({ urlParams, location }: ServiceMetricsProps) {
|
||||||
|
const { serviceName, start, end, errorGroupId, kuery } = urlParams;
|
||||||
|
const { data: errorDistributionData } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadErrorDistribution({ serviceName, start, end, errorGroupId, kuery }),
|
||||||
|
[serviceName, start, end, errorGroupId, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: transactionOverviewChartsData } = useTransactionOverviewCharts(
|
||||||
|
urlParams
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: serviceMetricChartData } = useServiceMetricCharts(urlParams);
|
||||||
|
|
||||||
|
if (!errorDistributionData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TransactionOverviewChartsRequestForAllTypes
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<TransactionCharts
|
<TransactionCharts
|
||||||
charts={data}
|
charts={transactionOverviewChartsData}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
location={location}
|
location={location}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
<EuiFlexGroup>
|
<EuiFlexGroup>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<ErrorDistributionRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<ErrorDistribution
|
<ErrorDistribution
|
||||||
distribution={data}
|
distribution={errorDistributionData}
|
||||||
title={i18n.translate(
|
title={i18n.translate(
|
||||||
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
|
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
|
||||||
{
|
{
|
||||||
|
@ -60,25 +70,19 @@ export function ServiceMetrics({ urlParams, location }: ServiceMetricsProps) {
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
<MetricsChartDataRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => {
|
|
||||||
return (
|
|
||||||
<SyncChartGroup
|
<SyncChartGroup
|
||||||
render={hoverXHandlers => (
|
render={hoverXHandlers => (
|
||||||
<EuiFlexGrid columns={2}>
|
<EuiFlexGrid columns={2}>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<CPUUsageChart
|
<CPUUsageChart
|
||||||
data={data.cpu}
|
data={serviceMetricChartData.cpu}
|
||||||
hoverXHandlers={hoverXHandlers}
|
hoverXHandlers={hoverXHandlers}
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
|
@ -86,7 +90,7 @@ export function ServiceMetrics({ urlParams, location }: ServiceMetricsProps) {
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<MemoryUsageChart
|
<MemoryUsageChart
|
||||||
data={data.memory}
|
data={serviceMetricChartData.memory}
|
||||||
hoverXHandlers={hoverXHandlers}
|
hoverXHandlers={hoverXHandlers}
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
|
@ -94,9 +98,6 @@ export function ServiceMetrics({ urlParams, location }: ServiceMetricsProps) {
|
||||||
</EuiFlexGrid>
|
</EuiFlexGrid>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EuiSpacer size="xxl" />
|
<EuiSpacer size="xxl" />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -6,28 +6,33 @@
|
||||||
|
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import { ServiceDetailsRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/serviceDetails';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { useFetcher } from '../../../hooks/useFetcher';
|
||||||
|
import { loadServiceDetails } from '../../../services/rest/apm/services';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { FilterBar } from '../../shared/FilterBar';
|
import { FilterBar } from '../../shared/FilterBar';
|
||||||
import { ServiceDetailTabs } from './ServiceDetailTabs';
|
import { ServiceDetailTabs } from './ServiceDetailTabs';
|
||||||
import { ServiceIntegrations } from './ServiceIntegrations';
|
import { ServiceIntegrations } from './ServiceIntegrations';
|
||||||
|
|
||||||
interface ServiceDetailsProps {
|
interface Props {
|
||||||
urlParams: IUrlParams;
|
urlParams: IUrlParams;
|
||||||
location: Location;
|
location: Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceDetailsView extends React.Component<ServiceDetailsProps> {
|
export function ServiceDetailsView({ urlParams, location }: Props) {
|
||||||
public render() {
|
const { serviceName, start, end, kuery } = urlParams;
|
||||||
const { urlParams, location } = this.props;
|
const { data: serviceDetailsData } = useFetcher(
|
||||||
|
() => loadServiceDetails({ serviceName, start, end, kuery }),
|
||||||
|
[serviceName, start, end, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!serviceDetailsData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServiceDetailsRequest
|
<React.Fragment>
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<EuiFlexGroup justifyContent="spaceBetween">
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiTitle size="l">
|
<EuiTitle size="l">
|
||||||
|
@ -36,8 +41,8 @@ export class ServiceDetailsView extends React.Component<ServiceDetailsProps> {
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<ServiceIntegrations
|
<ServiceIntegrations
|
||||||
transactionTypes={data.types}
|
transactionTypes={serviceDetailsData.types}
|
||||||
location={this.props.location}
|
location={location}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
@ -50,12 +55,8 @@ export class ServiceDetailsView extends React.Component<ServiceDetailsProps> {
|
||||||
<ServiceDetailTabs
|
<ServiceDetailTabs
|
||||||
location={location}
|
location={location}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
transactionTypes={data.types}
|
transactionTypes={serviceDetailsData.types}
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { asDecimal, asMillis } from '../../../../utils/formatters';
|
||||||
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: IServiceListItem[];
|
items?: IServiceListItem[];
|
||||||
noItemsMessage?: React.ReactNode;
|
noItemsMessage?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,77 +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 React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { ServiceOverview } from '../view';
|
|
||||||
import { STATUS } from '../../../../constants';
|
|
||||||
import * as apmRestServices from '../../../../services/rest/apm/status_check';
|
|
||||||
jest.mock('../../../../services/rest/apm/status_check');
|
|
||||||
|
|
||||||
describe('Service Overview -> View', () => {
|
|
||||||
let mockAgentStatus;
|
|
||||||
let wrapper;
|
|
||||||
let instance;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockAgentStatus = {
|
|
||||||
dataFound: true
|
|
||||||
};
|
|
||||||
|
|
||||||
apmRestServices.loadAgentStatus = jest.fn(() =>
|
|
||||||
Promise.resolve(mockAgentStatus)
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = shallow(<ServiceOverview serviceList={{ data: [] }} />);
|
|
||||||
instance = wrapper.instance();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render when historical data is found', () => {
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
const List = wrapper
|
|
||||||
.find('ServiceListRequest')
|
|
||||||
.props()
|
|
||||||
.render({});
|
|
||||||
expect(List.props).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render when historical data is not found', () => {
|
|
||||||
wrapper.setState({ historicalDataFound: false });
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
const List = wrapper
|
|
||||||
.find('ServiceListRequest')
|
|
||||||
.props()
|
|
||||||
.render({});
|
|
||||||
expect(List.props).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should check for historical data once', () => {});
|
|
||||||
|
|
||||||
describe('checking for historical data', () => {
|
|
||||||
it('should set historical data to true if data is found', async () => {
|
|
||||||
const props = {
|
|
||||||
serviceList: {
|
|
||||||
status: STATUS.SUCCESS,
|
|
||||||
data: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await instance.checkForHistoricalData(props);
|
|
||||||
expect(wrapper.state('historicalDataFound')).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set historical data state to false if data is NOT found', async () => {
|
|
||||||
const props = {
|
|
||||||
serviceList: {
|
|
||||||
status: STATUS.SUCCESS,
|
|
||||||
data: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mockAgentStatus.dataFound = false;
|
|
||||||
await instance.checkForHistoricalData(props);
|
|
||||||
expect(wrapper.state('historicalDataFound')).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { render, wait, waitForElement } from 'react-testing-library';
|
||||||
|
import 'react-testing-library/cleanup-after-each';
|
||||||
|
import * as apmRestServices from 'x-pack/plugins/apm/public/services/rest/apm/services';
|
||||||
|
// @ts-ignore
|
||||||
|
import configureStore from 'x-pack/plugins/apm/public/store/config/configureStore';
|
||||||
|
import * as statusCheck from '../../../../services/rest/apm/status_check';
|
||||||
|
import { ServiceOverview } from '../view';
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ServiceOverview urlParams={{}} />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Service Overview -> View', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
|
||||||
|
/* tslint:disable:no-console */
|
||||||
|
const originalError = console.error;
|
||||||
|
beforeAll(() => {
|
||||||
|
console.error = jest.fn();
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
console.error = originalError;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render services, when list is not empty', async () => {
|
||||||
|
// mock rest requests
|
||||||
|
const spy1 = jest
|
||||||
|
.spyOn(statusCheck, 'loadAgentStatus')
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
const spy2 = jest
|
||||||
|
.spyOn(apmRestServices, 'loadServiceList')
|
||||||
|
.mockResolvedValue([
|
||||||
|
{
|
||||||
|
serviceName: 'My Python Service',
|
||||||
|
agentName: 'python',
|
||||||
|
transactionsPerMinute: 100,
|
||||||
|
errorsPerMinute: 200,
|
||||||
|
avgResponseTime: 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'My Go Service',
|
||||||
|
agentName: 'go',
|
||||||
|
transactionsPerMinute: 400,
|
||||||
|
errorsPerMinute: 500,
|
||||||
|
avgResponseTime: 600
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { container, getByText } = render(<Comp />);
|
||||||
|
|
||||||
|
// wait for requests to be made
|
||||||
|
await wait(
|
||||||
|
() =>
|
||||||
|
expect(spy1).toHaveBeenCalledTimes(1) &&
|
||||||
|
expect(spy2).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 () => {
|
||||||
|
// mock rest requests
|
||||||
|
const spy1 = jest
|
||||||
|
.spyOn(statusCheck, 'loadAgentStatus')
|
||||||
|
.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const spy2 = jest
|
||||||
|
.spyOn(apmRestServices, 'loadServiceList')
|
||||||
|
.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { container, getByText } = render(<Comp />);
|
||||||
|
|
||||||
|
// wait for requests to be made
|
||||||
|
await wait(
|
||||||
|
() =>
|
||||||
|
expect(spy1).toHaveBeenCalledTimes(1) &&
|
||||||
|
expect(spy2).toHaveBeenCalledTimes(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
// wait for elements to be rendered
|
||||||
|
await waitForElement(() =>
|
||||||
|
getByText(
|
||||||
|
"Looks like you don't have any APM services installed. Let's add some!"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render empty message, when list is empty and historical data is found', async () => {
|
||||||
|
// mock rest requests
|
||||||
|
const spy1 = jest
|
||||||
|
.spyOn(statusCheck, 'loadAgentStatus')
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
const spy2 = jest
|
||||||
|
.spyOn(apmRestServices, 'loadServiceList')
|
||||||
|
.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { container, getByText } = render(<Comp />);
|
||||||
|
|
||||||
|
// wait for requests to be made
|
||||||
|
await wait(
|
||||||
|
() =>
|
||||||
|
expect(spy1).toHaveBeenCalledTimes(1) &&
|
||||||
|
expect(spy2).toHaveBeenCalledTimes(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
// wait for elements to be rendered
|
||||||
|
await waitForElement(() => getByText('No services found'));
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,43 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Service Overview -> View should render when historical data is found 1`] = `
|
|
||||||
<EuiPanel
|
|
||||||
grow={true}
|
|
||||||
hasShadow={false}
|
|
||||||
paddingSize="m"
|
|
||||||
>
|
|
||||||
<ServiceListRequest
|
|
||||||
render={[Function]}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Service Overview -> View should render when historical data is found 2`] = `
|
|
||||||
Object {
|
|
||||||
"items": Array [],
|
|
||||||
"noItemsMessage": <NoServicesMessage
|
|
||||||
historicalDataFound={true}
|
|
||||||
/>,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Service Overview -> View should render when historical data is not found 1`] = `
|
|
||||||
<EuiPanel
|
|
||||||
grow={true}
|
|
||||||
hasShadow={false}
|
|
||||||
paddingSize="m"
|
|
||||||
>
|
|
||||||
<ServiceListRequest
|
|
||||||
render={[Function]}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Service Overview -> View should render when historical data is not found 2`] = `
|
|
||||||
Object {
|
|
||||||
"items": Array [],
|
|
||||||
"noItemsMessage": <NoServicesMessage
|
|
||||||
historicalDataFound={false}
|
|
||||||
/>,
|
|
||||||
}
|
|
||||||
`;
|
|
|
@ -0,0 +1,296 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Service Overview -> View should render empty message, when list is empty and historical data is found 1`] = `
|
||||||
|
NodeList [
|
||||||
|
<tr
|
||||||
|
class="euiTableRow"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell euiTableRowCell--isMobileFullWidth"
|
||||||
|
colspan="5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignCenter"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiTableCellContent__text"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiEmptyPrompt"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiTextColor euiTextColor--subdued"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTitle euiTitle--small"
|
||||||
|
>
|
||||||
|
No services found
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiSpacer euiSpacer--m"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Service Overview -> View should render getting started message, when list is empty and no historical data is found 1`] = `
|
||||||
|
NodeList [
|
||||||
|
<tr
|
||||||
|
class="euiTableRow"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell euiTableRowCell--isMobileFullWidth"
|
||||||
|
colspan="5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignCenter"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiTableCellContent__text"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiEmptyPrompt"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiTextColor euiTextColor--subdued"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTitle euiTitle--small"
|
||||||
|
>
|
||||||
|
Looks like you don't have any APM services installed. Let's add some!
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiSpacer euiSpacer--m"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="euiText euiText--medium"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Upgrading from a pre-7.x version? Make sure you've also upgraded
|
||||||
|
your APM server instance(s) to at least 7.0.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You may also have old data that needs to be migrated.
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="euiLink euiLink--primary"
|
||||||
|
href="/app/kibana#/management/elasticsearch/upgrade_assistant?"
|
||||||
|
>
|
||||||
|
Learn more by visiting the Kibana Upgrade Assistant
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="euiSpacer euiSpacer--l"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="euiSpacer euiSpacer--s"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
class="euiLink euiLink--primary"
|
||||||
|
href="/app/kibana#/home/tutorial/apm?"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="euiButton euiButton--primary euiButton--small euiButton--fill"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiButton__content"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiButton__text"
|
||||||
|
>
|
||||||
|
Setup Instructions
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Service Overview -> View should render services, when list is not empty 1`] = `
|
||||||
|
NodeList [
|
||||||
|
<tr
|
||||||
|
class="euiTableRow"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiToolTipAnchor"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-describedby="service-name-tooltip"
|
||||||
|
class="euiLink euiLink--primary sc-bdVaJa eQDnXY"
|
||||||
|
href="#/My Go Service/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0"
|
||||||
|
>
|
||||||
|
My Go Service
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Agent
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
go
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Avg. response time
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
1 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Trans. per minute
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
400.0 tpm
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Errors per minute
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
500.0 err.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
<tr
|
||||||
|
class="euiTableRow"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="euiToolTipAnchor"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-describedby="service-name-tooltip"
|
||||||
|
class="euiLink euiLink--primary sc-bdVaJa eQDnXY"
|
||||||
|
href="#/My Python Service/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0"
|
||||||
|
>
|
||||||
|
My Python Service
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Agent
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
python
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Avg. response time
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
0 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Trans. per minute
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
100.0 tpm
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="euiTableRowCell"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||||
|
>
|
||||||
|
Errors per minute
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
|
||||||
|
>
|
||||||
|
200.0 err.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
]
|
||||||
|
`;
|
|
@ -5,14 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { getServiceList } from 'x-pack/plugins/apm/public/store/reactReduxRequest/serviceList';
|
|
||||||
import { IReduxState } from 'x-pack/plugins/apm/public/store/rootReducer';
|
import { IReduxState } from 'x-pack/plugins/apm/public/store/rootReducer';
|
||||||
import { getUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
import { getUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
import { ServiceOverview as View } from './view';
|
import { ServiceOverview as View } from './view';
|
||||||
|
|
||||||
function mapStateToProps(state = {} as IReduxState) {
|
function mapStateToProps(state = {} as IReduxState) {
|
||||||
return {
|
return {
|
||||||
serviceList: getServiceList(state),
|
|
||||||
urlParams: getUrlParams(state)
|
urlParams: getUrlParams(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,59 +5,32 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiPanel } from '@elastic/eui';
|
import { EuiPanel } from '@elastic/eui';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { RRRRenderResponse } from 'react-redux-request';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services';
|
import { useFetcher } from '../../../hooks/useFetcher';
|
||||||
|
import { loadServiceList } from '../../../services/rest/apm/services';
|
||||||
import { loadAgentStatus } from '../../../services/rest/apm/status_check';
|
import { loadAgentStatus } from '../../../services/rest/apm/status_check';
|
||||||
import { ServiceListRequest } from '../../../store/reactReduxRequest/serviceList';
|
|
||||||
import { NoServicesMessage } from './NoServicesMessage';
|
import { NoServicesMessage } from './NoServicesMessage';
|
||||||
import { ServiceList } from './ServiceList';
|
import { ServiceList } from './ServiceList';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
urlParams: IUrlParams;
|
urlParams: IUrlParams;
|
||||||
serviceList: RRRRenderResponse<IServiceListItem[]>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export function ServiceOverview({ urlParams }: Props) {
|
||||||
// any data submitted from APM agents found (not just in the given time range)
|
const { start, end, kuery } = urlParams;
|
||||||
historicalDataFound: boolean;
|
const { data: agentStatus = true } = useFetcher(() => loadAgentStatus(), []);
|
||||||
}
|
const { data: serviceListData } = useFetcher(
|
||||||
|
() => loadServiceList({ start, end, kuery }),
|
||||||
|
[start, end, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
export class ServiceOverview extends Component<Props, State> {
|
|
||||||
public state = { historicalDataFound: true };
|
|
||||||
|
|
||||||
public async checkForHistoricalData() {
|
|
||||||
const result = await loadAgentStatus();
|
|
||||||
this.setState({ historicalDataFound: result.dataFound });
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount() {
|
|
||||||
this.checkForHistoricalData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const { urlParams } = this.props;
|
|
||||||
|
|
||||||
// Render method here uses this.props.serviceList instead of received "data" from RRR
|
|
||||||
// to make it easier to test -- mapStateToProps uses the RRR selector so the data
|
|
||||||
// is the same either way
|
|
||||||
return (
|
return (
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<ServiceListRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={() => (
|
|
||||||
<ServiceList
|
<ServiceList
|
||||||
items={this.props.serviceList.data}
|
items={serviceListData}
|
||||||
noItemsMessage={
|
noItemsMessage={<NoServicesMessage historicalDataFound={agentStatus} />}
|
||||||
<NoServicesMessage
|
|
||||||
historicalDataFound={this.state.historicalDataFound}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import styled from 'styled-components';
|
||||||
import { ITransactionGroup } from 'x-pack/plugins/apm/server/lib/transaction_groups/transform';
|
import { ITransactionGroup } from 'x-pack/plugins/apm/server/lib/transaction_groups/transform';
|
||||||
import { fontSizes, truncate } from '../../../style/variables';
|
import { fontSizes, truncate } from '../../../style/variables';
|
||||||
import { asMillis } from '../../../utils/formatters';
|
import { asMillis } from '../../../utils/formatters';
|
||||||
|
import { EmptyMessage } from '../../shared/EmptyMessage';
|
||||||
import { ImpactBar } from '../../shared/ImpactBar';
|
import { ImpactBar } from '../../shared/ImpactBar';
|
||||||
import { TransactionLink } from '../../shared/Links/TransactionLink';
|
import { TransactionLink } from '../../shared/Links/TransactionLink';
|
||||||
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
|
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
|
||||||
|
@ -22,7 +23,6 @@ const StyledTransactionLink = styled(TransactionLink)`
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: ITransactionGroup[];
|
items: ITransactionGroup[];
|
||||||
noItemsMessage: React.ReactNode;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,15 @@ const traceListColumns: Array<ITableColumn<ITransactionGroup>> = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export function TraceList({ items = [], noItemsMessage, isLoading }: Props) {
|
const noItemsMessage = (
|
||||||
|
<EmptyMessage
|
||||||
|
heading={i18n.translate('xpack.apm.tracesTable.notFoundLabel', {
|
||||||
|
defaultMessage: 'No traces found for this query'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export function TraceList({ items = [], isLoading }: Props) {
|
||||||
const noItems = isLoading ? null : noItemsMessage;
|
const noItems = isLoading ? null : noItemsMessage;
|
||||||
return (
|
return (
|
||||||
<ManagedTable
|
<ManagedTable
|
||||||
|
|
|
@ -5,39 +5,26 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiPanel } from '@elastic/eui';
|
import { EuiPanel } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RRRRenderResponse } from 'react-redux-request';
|
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
|
||||||
import { TraceListAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_top_traces';
|
import { loadTraceList } from '../../../services/rest/apm/traces';
|
||||||
import { TraceListRequest } from '../../../store/reactReduxRequest/traceList';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { EmptyMessage } from '../../shared/EmptyMessage';
|
|
||||||
import { TraceList } from './TraceList';
|
import { TraceList } from './TraceList';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
urlParams: object;
|
urlParams: IUrlParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TraceOverview(props: Props) {
|
export function TraceOverview(props: Props) {
|
||||||
const { urlParams } = props;
|
const { start, end, kuery } = props.urlParams;
|
||||||
|
const { status, data = [] } = useFetcher(
|
||||||
|
() => loadTraceList({ start, end, kuery }),
|
||||||
|
[start, end, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<TraceListRequest
|
<TraceList items={data} isLoading={status === FETCH_STATUS.LOADING} />
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data, status }: RRRRenderResponse<TraceListAPIResponse>) => (
|
|
||||||
<TraceList
|
|
||||||
items={data}
|
|
||||||
isLoading={status === 'LOADING'}
|
|
||||||
noItemsMessage={
|
|
||||||
<EmptyMessage
|
|
||||||
heading={i18n.translate('xpack.apm.tracesTable.notFoundLabel', {
|
|
||||||
defaultMessage: 'No traces found for this query'
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
location: Location;
|
location: Location;
|
||||||
distribution: ITransactionDistributionAPIResponse;
|
distribution?: ITransactionDistributionAPIResponse;
|
||||||
urlParams: IUrlParams;
|
urlParams: IUrlParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,9 +95,45 @@ export class TransactionDistribution extends Component<Props> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public redirectToTransactionType() {
|
||||||
|
const { urlParams, location, distribution } = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!distribution ||
|
||||||
|
!distribution.defaultSample ||
|
||||||
|
urlParams.traceId ||
|
||||||
|
urlParams.transactionId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { traceId, transactionId } = distribution.defaultSample;
|
||||||
|
|
||||||
|
history.replace({
|
||||||
|
...location,
|
||||||
|
search: fromQuery({
|
||||||
|
...toQuery(location.search),
|
||||||
|
traceId,
|
||||||
|
transactionId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.redirectToTransactionType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
this.redirectToTransactionType();
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { location, distribution, urlParams } = this.props;
|
const { location, distribution, urlParams } = this.props;
|
||||||
|
|
||||||
|
if (!distribution || !urlParams.traceId || !urlParams.transactionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const buckets = getFormattedBuckets(
|
const buckets = getFormattedBuckets(
|
||||||
distribution.buckets,
|
distribution.buckets,
|
||||||
distribution.bucketSize
|
distribution.bucketSize
|
||||||
|
@ -163,7 +199,7 @@ export class TransactionDistribution extends Component<Props> {
|
||||||
bucketIndex={bucketIndex}
|
bucketIndex={bucketIndex}
|
||||||
onClick={(bucket: IChartPoint) => {
|
onClick={(bucket: IChartPoint) => {
|
||||||
if (bucket.sample && bucket.y > 0) {
|
if (bucket.sample && bucket.y > 0) {
|
||||||
history.replace({
|
history.push({
|
||||||
...location,
|
...location,
|
||||||
search: fromQuery({
|
search: fromQuery({
|
||||||
...toQuery(location.search),
|
...toQuery(location.search),
|
||||||
|
|
|
@ -96,13 +96,12 @@ describe('waterfall_helpers', () => {
|
||||||
|
|
||||||
it('should return full waterfall', () => {
|
it('should return full waterfall', () => {
|
||||||
const entryTransactionId = 'myTransactionId1';
|
const entryTransactionId = 'myTransactionId1';
|
||||||
const errorCountsByTransactionId = {
|
const errorsPerTransaction = {
|
||||||
myTransactionId1: 2,
|
myTransactionId1: 2,
|
||||||
myTransactionId2: 3
|
myTransactionId2: 3
|
||||||
};
|
};
|
||||||
const waterfall = getWaterfall(
|
const waterfall = getWaterfall(
|
||||||
hits,
|
{ trace: hits, errorsPerTransaction },
|
||||||
errorCountsByTransactionId,
|
|
||||||
entryTransactionId
|
entryTransactionId
|
||||||
);
|
);
|
||||||
expect(waterfall.orderedItems.length).toBe(6);
|
expect(waterfall.orderedItems.length).toBe(6);
|
||||||
|
@ -112,13 +111,12 @@ describe('waterfall_helpers', () => {
|
||||||
|
|
||||||
it('should return partial waterfall', () => {
|
it('should return partial waterfall', () => {
|
||||||
const entryTransactionId = 'myTransactionId2';
|
const entryTransactionId = 'myTransactionId2';
|
||||||
const errorCountsByTransactionId = {
|
const errorsPerTransaction = {
|
||||||
myTransactionId1: 2,
|
myTransactionId1: 2,
|
||||||
myTransactionId2: 3
|
myTransactionId2: 3
|
||||||
};
|
};
|
||||||
const waterfall = getWaterfall(
|
const waterfall = getWaterfall(
|
||||||
hits,
|
{ trace: hits, errorsPerTransaction },
|
||||||
errorCountsByTransactionId,
|
|
||||||
entryTransactionId
|
entryTransactionId
|
||||||
);
|
);
|
||||||
expect(waterfall.orderedItems.length).toBe(4);
|
expect(waterfall.orderedItems.length).toBe(4);
|
||||||
|
@ -128,13 +126,12 @@ describe('waterfall_helpers', () => {
|
||||||
|
|
||||||
it('getTransactionById', () => {
|
it('getTransactionById', () => {
|
||||||
const entryTransactionId = 'myTransactionId1';
|
const entryTransactionId = 'myTransactionId1';
|
||||||
const errorCountsByTransactionId = {
|
const errorsPerTransaction = {
|
||||||
myTransactionId1: 2,
|
myTransactionId1: 2,
|
||||||
myTransactionId2: 3
|
myTransactionId2: 3
|
||||||
};
|
};
|
||||||
const waterfall = getWaterfall(
|
const waterfall = getWaterfall(
|
||||||
hits,
|
{ trace: hits, errorsPerTransaction },
|
||||||
errorCountsByTransactionId,
|
|
||||||
entryTransactionId
|
entryTransactionId
|
||||||
);
|
);
|
||||||
const transaction = waterfall.getTransactionById('myTransactionId2');
|
const transaction = waterfall.getTransactionById('myTransactionId2');
|
||||||
|
|
|
@ -239,8 +239,7 @@ function createGetTransactionById(itemsById: IWaterfallIndex) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWaterfall(
|
export function getWaterfall(
|
||||||
trace: TraceAPIResponse['trace'],
|
{ trace, errorsPerTransaction }: TraceAPIResponse,
|
||||||
errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'],
|
|
||||||
entryTransactionId?: Transaction['transaction']['id']
|
entryTransactionId?: Transaction['transaction']['id']
|
||||||
): IWaterfall {
|
): IWaterfall {
|
||||||
if (isEmpty(trace) || !entryTransactionId) {
|
if (isEmpty(trace) || !entryTransactionId) {
|
||||||
|
|
|
@ -7,10 +7,11 @@
|
||||||
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TransactionDetailsChartsRequest } from '../../../store/reactReduxRequest/transactionDetailsCharts';
|
import { useTransactionDetailsCharts } from '../../../hooks/useTransactionDetailsCharts';
|
||||||
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
|
import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution';
|
||||||
import { WaterfallRequest } from '../../../store/reactReduxRequest/waterfall';
|
import { useWaterfall } from '../../../hooks/useWaterfall';
|
||||||
import { IUrlParams } from '../../../store/urlParams';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
|
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
|
||||||
import { EmptyMessage } from '../../shared/EmptyMessage';
|
import { EmptyMessage } from '../../shared/EmptyMessage';
|
||||||
|
@ -19,12 +20,18 @@ import { TransactionDistribution } from './Distribution';
|
||||||
import { Transaction } from './Transaction';
|
import { Transaction } from './Transaction';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mlAvailable: boolean;
|
|
||||||
urlParams: IUrlParams;
|
urlParams: IUrlParams;
|
||||||
location: Location;
|
location: Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TransactionDetailsView({ urlParams, location }: Props) {
|
export function TransactionDetailsView({ urlParams, location }: Props) {
|
||||||
|
const { data: distributionData } = useTransactionDistribution(urlParams);
|
||||||
|
const { data: transactionDetailsChartsData } = useTransactionDetailsCharts(
|
||||||
|
urlParams
|
||||||
|
);
|
||||||
|
const { data: waterfall } = useWaterfall(urlParams);
|
||||||
|
const transaction = waterfall.getTransactionById(urlParams.transactionId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<EuiTitle size="l">
|
<EuiTitle size="l">
|
||||||
|
@ -32,47 +39,28 @@ export function TransactionDetailsView({ urlParams, location }: Props) {
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
|
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
|
|
||||||
<FilterBar />
|
<FilterBar />
|
||||||
|
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
|
|
||||||
<TransactionDetailsChartsRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<TransactionCharts
|
<TransactionCharts
|
||||||
charts={data}
|
charts={transactionDetailsChartsData}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
location={location}
|
location={location}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
|
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<TransactionDistributionRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<TransactionDistribution
|
<TransactionDistribution
|
||||||
distribution={data}
|
distribution={distributionData}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
location={location}
|
location={location}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
|
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
<WaterfallRequest
|
|
||||||
urlParams={urlParams}
|
{!transaction ? (
|
||||||
traceId={urlParams.traceId}
|
|
||||||
render={({ data: waterfall }) => {
|
|
||||||
const transaction = waterfall.getTransactionById(
|
|
||||||
urlParams.transactionId
|
|
||||||
);
|
|
||||||
if (!transaction) {
|
|
||||||
return (
|
|
||||||
<EmptyMessage
|
<EmptyMessage
|
||||||
heading={i18n.translate(
|
heading={i18n.translate(
|
||||||
'xpack.apm.transactionDetails.noTransactionTitle',
|
'xpack.apm.transactionDetails.noTransactionTitle',
|
||||||
|
@ -88,19 +76,14 @@ export function TransactionDetailsView({ urlParams, location }: Props) {
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
) : (
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transaction
|
<Transaction
|
||||||
location={location}
|
location={location}
|
||||||
transaction={transaction}
|
transaction={transaction}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
waterfall={waterfall}
|
waterfall={waterfall}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +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 React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { TransactionOverviewView } from '..';
|
|
||||||
|
|
||||||
jest.mock(
|
|
||||||
'ui/chrome',
|
|
||||||
() => ({
|
|
||||||
getBasePath: () => `/some/base/path`,
|
|
||||||
getInjected: key => {
|
|
||||||
if (key === 'mlEnabled') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
throw new Error(`inexpected key ${key}`);
|
|
||||||
},
|
|
||||||
getUiSettingsClient: () => {
|
|
||||||
return {
|
|
||||||
get: key => {
|
|
||||||
switch (key) {
|
|
||||||
case 'timepicker:timeDefaults':
|
|
||||||
return { from: 'now-15m', to: 'now', mode: 'quick' };
|
|
||||||
case 'timepicker:refreshIntervalDefaults':
|
|
||||||
return { display: 'Off', pause: false, value: 0 };
|
|
||||||
default:
|
|
||||||
throw new Error(`Unexpected config key: ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{ virtual: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const setup = () => {
|
|
||||||
const props = {
|
|
||||||
agentName: 'test-agent',
|
|
||||||
serviceName: 'test-service',
|
|
||||||
serviceTransactionTypes: ['a', 'b'],
|
|
||||||
location: {},
|
|
||||||
history: {
|
|
||||||
push: jest.fn()
|
|
||||||
},
|
|
||||||
urlParams: { transactionType: 'test-type', serviceName: 'MyServiceName' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<TransactionOverviewView {...props} />);
|
|
||||||
return { props, wrapper };
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('TransactionOverviewView', () => {
|
|
||||||
it('should render null if there is no transaction type in the search string', () => {
|
|
||||||
const { wrapper } = setup();
|
|
||||||
wrapper.setProps({ urlParams: { serviceName: 'MyServiceName' } });
|
|
||||||
expect(wrapper).toMatchInlineSnapshot(`""`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render with type filter controls', () => {
|
|
||||||
const { wrapper } = setup();
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render without type filter controls if there is just a single type', () => {
|
|
||||||
const { wrapper } = setup();
|
|
||||||
wrapper.setProps({
|
|
||||||
serviceTransactionTypes: ['a']
|
|
||||||
});
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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 createHistory from 'history/createHashHistory';
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Router } from 'react-router-dom';
|
||||||
|
import { queryByLabelText, render } from 'react-testing-library';
|
||||||
|
// @ts-ignore
|
||||||
|
import configureStore from 'x-pack/plugins/apm/public/store/config/configureStore';
|
||||||
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { TransactionOverview } from '..';
|
||||||
|
|
||||||
|
function setup(props: {
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
serviceTransactionTypes: string[];
|
||||||
|
}) {
|
||||||
|
const store = configureStore();
|
||||||
|
const history = createHistory();
|
||||||
|
history.replace = jest.fn();
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router history={history}>
|
||||||
|
<TransactionOverview {...props} />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return { container, history };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TransactionOverviewView', () => {
|
||||||
|
describe('when no transaction type is given', () => {
|
||||||
|
it('should render null', () => {
|
||||||
|
const { container } = setup({
|
||||||
|
serviceTransactionTypes: ['firstType', 'secondType'],
|
||||||
|
urlParams: {
|
||||||
|
serviceName: 'MyServiceName'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(container).toMatchInlineSnapshot(`<div />`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to first type', () => {
|
||||||
|
const { history } = setup({
|
||||||
|
serviceTransactionTypes: ['firstType', 'secondType'],
|
||||||
|
urlParams: {
|
||||||
|
serviceName: 'MyServiceName'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(history.replace).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
pathname: '/MyServiceName/transactions/firstType'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const FILTER_BY_TYPE_LABEL = 'Filter by type';
|
||||||
|
|
||||||
|
describe('when transactionType is selected and multiple transaction types are given', () => {
|
||||||
|
it('should render dropdown with transaction types', () => {
|
||||||
|
const { container } = setup({
|
||||||
|
serviceTransactionTypes: ['firstType', 'secondType'],
|
||||||
|
urlParams: {
|
||||||
|
transactionType: 'secondType',
|
||||||
|
serviceName: 'MyServiceName'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByLabelText(container, FILTER_BY_TYPE_LABEL)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a transaction type is selected, and there are no other transaction types', () => {
|
||||||
|
it('should not render a dropdown with transaction types', () => {
|
||||||
|
const { container } = setup({
|
||||||
|
serviceTransactionTypes: ['firstType'],
|
||||||
|
urlParams: {
|
||||||
|
transactionType: 'firstType',
|
||||||
|
serviceName: 'MyServiceName'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByLabelText(container, FILTER_BY_TYPE_LABEL)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,115 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`TransactionOverviewView should render with type filter controls 1`] = `
|
|
||||||
<Fragment>
|
|
||||||
<EuiFormRow
|
|
||||||
describedByIds={Array []}
|
|
||||||
fullWidth={false}
|
|
||||||
hasEmptyLabelSpace={false}
|
|
||||||
label="Filter by type"
|
|
||||||
labelType="label"
|
|
||||||
>
|
|
||||||
<EuiSelect
|
|
||||||
compressed={false}
|
|
||||||
fullWidth={false}
|
|
||||||
hasNoInitialSelection={false}
|
|
||||||
isLoading={false}
|
|
||||||
onChange={[Function]}
|
|
||||||
options={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"text": "a",
|
|
||||||
"value": "a",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"text": "b",
|
|
||||||
"value": "b",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
value="test-type"
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
<TransactionOverviewChartsRequest
|
|
||||||
render={[Function]}
|
|
||||||
urlParams={
|
|
||||||
Object {
|
|
||||||
"serviceName": "MyServiceName",
|
|
||||||
"transactionType": "test-type",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<EuiSpacer
|
|
||||||
size="l"
|
|
||||||
/>
|
|
||||||
<EuiPanel
|
|
||||||
grow={true}
|
|
||||||
hasShadow={false}
|
|
||||||
paddingSize="m"
|
|
||||||
>
|
|
||||||
<EuiTitle
|
|
||||||
size="xs"
|
|
||||||
textTransform="none"
|
|
||||||
>
|
|
||||||
<h3>
|
|
||||||
Transactions
|
|
||||||
</h3>
|
|
||||||
</EuiTitle>
|
|
||||||
<EuiSpacer
|
|
||||||
size="s"
|
|
||||||
/>
|
|
||||||
<TransactionListRequest
|
|
||||||
render={[Function]}
|
|
||||||
urlParams={
|
|
||||||
Object {
|
|
||||||
"serviceName": "MyServiceName",
|
|
||||||
"transactionType": "test-type",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`TransactionOverviewView should render without type filter controls if there is just a single type 1`] = `
|
|
||||||
<Fragment>
|
|
||||||
<TransactionOverviewChartsRequest
|
|
||||||
render={[Function]}
|
|
||||||
urlParams={
|
|
||||||
Object {
|
|
||||||
"serviceName": "MyServiceName",
|
|
||||||
"transactionType": "test-type",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<EuiSpacer
|
|
||||||
size="l"
|
|
||||||
/>
|
|
||||||
<EuiPanel
|
|
||||||
grow={true}
|
|
||||||
hasShadow={false}
|
|
||||||
paddingSize="m"
|
|
||||||
>
|
|
||||||
<EuiTitle
|
|
||||||
size="xs"
|
|
||||||
textTransform="none"
|
|
||||||
>
|
|
||||||
<h3>
|
|
||||||
Transactions
|
|
||||||
</h3>
|
|
||||||
</EuiTitle>
|
|
||||||
<EuiSpacer
|
|
||||||
size="s"
|
|
||||||
/>
|
|
||||||
<TransactionListRequest
|
|
||||||
render={[Function]}
|
|
||||||
urlParams={
|
|
||||||
Object {
|
|
||||||
"serviceName": "MyServiceName",
|
|
||||||
"transactionType": "test-type",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EuiPanel>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`TransactionOverviewView when transactionType is selected and multiple transaction types are given should render dropdown with transaction types 1`] = `
|
||||||
|
<select
|
||||||
|
class="euiSelect"
|
||||||
|
id="transaction-type-select-row"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="firstType"
|
||||||
|
>
|
||||||
|
firstType
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="secondType"
|
||||||
|
>
|
||||||
|
secondType
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
`;
|
|
@ -12,35 +12,65 @@ import {
|
||||||
EuiTitle
|
EuiTitle
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { Location } from 'history';
|
||||||
|
import { first } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||||
import { TransactionCharts } from 'x-pack/plugins/apm/public/components/shared/charts/TransactionCharts';
|
import { TransactionCharts } from 'x-pack/plugins/apm/public/components/shared/charts/TransactionCharts';
|
||||||
import { legacyEncodeURIComponent } from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
|
import { legacyEncodeURIComponent } from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
|
||||||
import { TransactionListRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/transactionList';
|
|
||||||
import { TransactionOverviewChartsRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/transactionOverviewCharts';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { useTransactionList } from '../../../hooks/useTransactionList';
|
||||||
|
import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts';
|
||||||
import { TransactionList } from './List';
|
import { TransactionList } from './List';
|
||||||
|
import { useRedirect } from './useRedirect';
|
||||||
|
|
||||||
interface TransactionOverviewProps extends RouteComponentProps {
|
interface TransactionOverviewProps extends RouteComponentProps {
|
||||||
urlParams: IUrlParams;
|
urlParams: IUrlParams;
|
||||||
serviceTransactionTypes: string[];
|
serviceTransactionTypes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TransactionOverviewView extends React.Component<
|
function getRedirectLocation({
|
||||||
TransactionOverviewProps
|
urlParams,
|
||||||
> {
|
location,
|
||||||
public handleTypeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
serviceTransactionTypes
|
||||||
const { urlParams, history, location } = this.props;
|
}: {
|
||||||
const type = legacyEncodeURIComponent(event.target.value);
|
location: Location;
|
||||||
history.push({
|
urlParams: IUrlParams;
|
||||||
...location,
|
serviceTransactionTypes: string[];
|
||||||
pathname: `/${urlParams.serviceName}/transactions/${type}`
|
}) {
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const { urlParams, serviceTransactionTypes, location } = this.props;
|
|
||||||
const { serviceName, transactionType } = urlParams;
|
const { serviceName, transactionType } = urlParams;
|
||||||
|
const firstTransactionType = first(serviceTransactionTypes);
|
||||||
|
if (!transactionType && firstTransactionType) {
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
pathname: `/${serviceName}/transactions/${firstTransactionType}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransactionOverviewView({
|
||||||
|
urlParams,
|
||||||
|
serviceTransactionTypes,
|
||||||
|
location,
|
||||||
|
history
|
||||||
|
}: TransactionOverviewProps) {
|
||||||
|
const { serviceName, transactionType } = urlParams;
|
||||||
|
|
||||||
|
// redirect to first transaction type
|
||||||
|
useRedirect(
|
||||||
|
history,
|
||||||
|
getRedirectLocation({
|
||||||
|
urlParams,
|
||||||
|
location,
|
||||||
|
serviceTransactionTypes
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: transactionOverviewCharts } = useTransactionOverviewCharts(
|
||||||
|
urlParams
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: transactionListData } = useTransactionList(urlParams);
|
||||||
|
|
||||||
// filtering by type is currently required
|
// filtering by type is currently required
|
||||||
if (!serviceName || !transactionType) {
|
if (!serviceName || !transactionType) {
|
||||||
|
@ -51,6 +81,7 @@ export class TransactionOverviewView extends React.Component<
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{serviceTransactionTypes.length > 1 ? (
|
{serviceTransactionTypes.length > 1 ? (
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
|
id="transaction-type-select-row"
|
||||||
label={i18n.translate(
|
label={i18n.translate(
|
||||||
'xpack.apm.transactionsTable.filterByTypeLabel',
|
'xpack.apm.transactionsTable.filterByTypeLabel',
|
||||||
{
|
{
|
||||||
|
@ -64,21 +95,22 @@ export class TransactionOverviewView extends React.Component<
|
||||||
value: type
|
value: type
|
||||||
}))}
|
}))}
|
||||||
value={transactionType}
|
value={transactionType}
|
||||||
onChange={this.handleTypeChange}
|
onChange={event => {
|
||||||
|
const type = legacyEncodeURIComponent(event.target.value);
|
||||||
|
history.push({
|
||||||
|
...location,
|
||||||
|
pathname: `/${urlParams.serviceName}/transactions/${type}`
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<TransactionOverviewChartsRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<TransactionCharts
|
<TransactionCharts
|
||||||
charts={data}
|
charts={transactionOverviewCharts}
|
||||||
location={location}
|
location={location}
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
|
@ -87,16 +119,13 @@ export class TransactionOverviewView extends React.Component<
|
||||||
<h3>Transactions</h3>
|
<h3>Transactions</h3>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
<TransactionListRequest
|
<TransactionList
|
||||||
urlParams={urlParams}
|
items={transactionListData}
|
||||||
render={({ data }) => (
|
serviceName={serviceName}
|
||||||
<TransactionList items={data} serviceName={serviceName} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const TransactionOverview = withRouter(TransactionOverviewView);
|
export const TransactionOverview = withRouter(TransactionOverviewView);
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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 { History, Location } from 'history';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useRedirect(history: History, redirectLocation?: Location) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (redirectLocation) {
|
||||||
|
history.replace(redirectLocation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||||
import { Store } from 'redux';
|
import { Store } from 'redux';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import configureStore from 'x-pack/plugins/apm/public/store/config/configureStore';
|
import configureStore from 'x-pack/plugins/apm/public/store/config/configureStore';
|
||||||
import { mockNow } from 'x-pack/plugins/apm/public/utils/testHelpers';
|
import { mockNow, tick } from 'x-pack/plugins/apm/public/utils/testHelpers';
|
||||||
import { DatePicker, DatePickerComponent } from '../DatePicker';
|
import { DatePicker, DatePickerComponent } from '../DatePicker';
|
||||||
|
|
||||||
function mountPicker(initialState = {}) {
|
function mountPicker(initialState = {}) {
|
||||||
|
@ -54,8 +54,6 @@ describe('DatePicker', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const tick = () => new Promise(resolve => setImmediate(resolve, 0));
|
|
||||||
|
|
||||||
describe('refresh cycle', () => {
|
describe('refresh cycle', () => {
|
||||||
let nowSpy: jest.Mock;
|
let nowSpy: jest.Mock;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -4,17 +4,220 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import {
|
||||||
import { selectHasMLJob } from 'x-pack/plugins/apm/public/store/reactReduxRequest/transactionOverviewCharts';
|
EuiFlexGrid,
|
||||||
import { IReduxState } from 'x-pack/plugins/apm/public/store/rootReducer';
|
EuiFlexGroup,
|
||||||
import { selectIsMLAvailable } from 'x-pack/plugins/apm/public/store/selectors/license';
|
EuiFlexItem,
|
||||||
import { TransactionChartsView } from './view';
|
EuiIconTip,
|
||||||
|
EuiPanel,
|
||||||
|
EuiText,
|
||||||
|
EuiTitle
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { Location } from 'history';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { MLJobLink } from 'x-pack/plugins/apm/public/components/shared/Links/MLJobLink';
|
||||||
|
import { ITransactionChartData } from 'x-pack/plugins/apm/public/store/selectors/chartSelectors';
|
||||||
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { Coordinate } from 'x-pack/plugins/apm/typings/timeseries';
|
||||||
|
import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
|
||||||
|
import { LicenseContext } from '../../../app/Main/LicenseCheck';
|
||||||
|
// @ts-ignore
|
||||||
|
import CustomPlot from '../CustomPlot';
|
||||||
|
import { SyncChartGroup } from '../SyncChartGroup';
|
||||||
|
|
||||||
const mapStateToProps = (state: IReduxState) => ({
|
interface TransactionChartProps {
|
||||||
mlAvailable: selectIsMLAvailable(state),
|
charts: ITransactionChartData;
|
||||||
hasMLJob: selectHasMLJob(state)
|
location: Location;
|
||||||
});
|
urlParams: IUrlParams;
|
||||||
|
}
|
||||||
|
|
||||||
export const TransactionCharts = connect(mapStateToProps)(
|
const ShiftedIconWrapper = styled.span`
|
||||||
TransactionChartsView
|
padding-right: 5px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
display: inline-block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ShiftedEuiText = styled(EuiText)`
|
||||||
|
position: relative;
|
||||||
|
top: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const msTimeUnitLabel = i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.msTimeUnitLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'ms'
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export class TransactionCharts extends Component<TransactionChartProps> {
|
||||||
|
public getResponseTimeTickFormatter = (t: number) => {
|
||||||
|
return this.props.charts.noHits ? `- ${msTimeUnitLabel}` : asMillis(t);
|
||||||
|
};
|
||||||
|
|
||||||
|
public getResponseTimeTooltipFormatter = (p: Coordinate) => {
|
||||||
|
return this.props.charts.noHits || !p
|
||||||
|
? `- ${msTimeUnitLabel}`
|
||||||
|
: asMillis(p.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
public getTPMFormatter = (t: number | null) => {
|
||||||
|
const { urlParams, charts } = this.props;
|
||||||
|
const unit = tpmUnit(urlParams.transactionType);
|
||||||
|
return charts.noHits || t === null
|
||||||
|
? `- ${unit}`
|
||||||
|
: `${asInteger(t)} ${unit}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getTPMTooltipFormatter = (p: Coordinate) => {
|
||||||
|
return this.getTPMFormatter(p.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
public renderMLHeader(mlAvailable: boolean) {
|
||||||
|
const hasMLJob = this.props.charts.hasMLJob;
|
||||||
|
if (!mlAvailable || !hasMLJob) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { serviceName, transactionType } = this.props.urlParams;
|
||||||
|
|
||||||
|
if (!serviceName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<ShiftedEuiText size="xs">
|
||||||
|
<ShiftedIconWrapper>
|
||||||
|
<EuiIconTip
|
||||||
|
content={i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.machineLearningTooltip',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores >= 75.'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ShiftedIconWrapper>
|
||||||
|
<span>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.machineLearningLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Machine learning:'
|
||||||
|
}
|
||||||
|
)}{' '}
|
||||||
|
</span>
|
||||||
|
<MLJobLink
|
||||||
|
serviceName={serviceName}
|
||||||
|
transactionType={transactionType}
|
||||||
|
location={this.props.location}
|
||||||
|
>
|
||||||
|
View Job
|
||||||
|
</MLJobLink>
|
||||||
|
</ShiftedEuiText>
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { charts, urlParams } = this.props;
|
||||||
|
const { noHits, responseTimeSeries, tpmSeries } = charts;
|
||||||
|
const { transactionType } = urlParams;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SyncChartGroup
|
||||||
|
render={hoverXHandlers => (
|
||||||
|
<EuiFlexGrid columns={2}>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiPanel>
|
||||||
|
<React.Fragment>
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<span>{responseTimeLabel(transactionType)}</span>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<LicenseContext.Consumer>
|
||||||
|
{license =>
|
||||||
|
this.renderMLHeader(license.features.ml.is_available)
|
||||||
|
}
|
||||||
|
</LicenseContext.Consumer>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<CustomPlot
|
||||||
|
noHits={noHits}
|
||||||
|
series={responseTimeSeries}
|
||||||
|
{...hoverXHandlers}
|
||||||
|
tickFormatY={this.getResponseTimeTickFormatter}
|
||||||
|
formatTooltipValue={this.getResponseTimeTooltipFormatter}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</EuiPanel>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem style={{ flexShrink: 1 }}>
|
||||||
|
<EuiPanel>
|
||||||
|
<React.Fragment>
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<span>{tpmLabel(transactionType)}</span>
|
||||||
|
</EuiTitle>
|
||||||
|
<CustomPlot
|
||||||
|
noHits={noHits}
|
||||||
|
series={tpmSeries}
|
||||||
|
{...hoverXHandlers}
|
||||||
|
tickFormatY={this.getTPMFormatter}
|
||||||
|
formatTooltipValue={this.getTPMTooltipFormatter}
|
||||||
|
truncateLegends
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
</EuiPanel>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGrid>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpmLabel(type?: string) {
|
||||||
|
return type === 'request'
|
||||||
|
? i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.requestsPerMinuteLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Requests per minute'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Transactions per minute'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseTimeLabel(type?: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'page-load':
|
||||||
|
return i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.pageLoadTimesLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Page load times'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
case 'route-change':
|
||||||
|
return i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.routeChangeTimesLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Route change times'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return i18n.translate(
|
||||||
|
'xpack.apm.metrics.transactionChart.transactionDurationLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Transaction duration'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,224 +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 {
|
|
||||||
EuiFlexGrid,
|
|
||||||
EuiFlexGroup,
|
|
||||||
EuiFlexItem,
|
|
||||||
EuiIconTip,
|
|
||||||
EuiPanel,
|
|
||||||
EuiText,
|
|
||||||
EuiTitle
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { Location } from 'history';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { MLJobLink } from 'x-pack/plugins/apm/public/components/shared/Links/MLJobLink';
|
|
||||||
import { ITransactionChartData } from 'x-pack/plugins/apm/public/store/selectors/chartSelectors';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
|
||||||
import { Coordinate } from 'x-pack/plugins/apm/typings/timeseries';
|
|
||||||
import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
|
|
||||||
// @ts-ignore
|
|
||||||
import CustomPlot from '../CustomPlot';
|
|
||||||
import { SyncChartGroup } from '../SyncChartGroup';
|
|
||||||
|
|
||||||
interface TransactionChartProps {
|
|
||||||
charts: ITransactionChartData;
|
|
||||||
chartWrapper?: React.ComponentClass | React.SFC;
|
|
||||||
location: Location;
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
mlAvailable: boolean;
|
|
||||||
hasMLJob: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShiftedIconWrapper = styled.span`
|
|
||||||
padding-right: 5px;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
display: inline-block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ShiftedEuiText = styled(EuiText)`
|
|
||||||
position: relative;
|
|
||||||
top: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const msTimeUnitLabel = i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.msTimeUnitLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'ms'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export class TransactionChartsView extends Component<TransactionChartProps> {
|
|
||||||
public getResponseTimeTickFormatter = (t: number) => {
|
|
||||||
return this.props.charts.noHits ? `- ${msTimeUnitLabel}` : asMillis(t);
|
|
||||||
};
|
|
||||||
|
|
||||||
public getResponseTimeTooltipFormatter = (p: Coordinate) => {
|
|
||||||
return this.props.charts.noHits || !p
|
|
||||||
? `- ${msTimeUnitLabel}`
|
|
||||||
: asMillis(p.y);
|
|
||||||
};
|
|
||||||
|
|
||||||
public getTPMFormatter = (t: number | null) => {
|
|
||||||
const { urlParams, charts } = this.props;
|
|
||||||
const unit = tpmUnit(urlParams.transactionType);
|
|
||||||
return charts.noHits || t === null
|
|
||||||
? `- ${unit}`
|
|
||||||
: `${asInteger(t)} ${unit}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
public getTPMTooltipFormatter = (p: Coordinate) => {
|
|
||||||
return this.getTPMFormatter(p.y);
|
|
||||||
};
|
|
||||||
|
|
||||||
public renderMLHeader() {
|
|
||||||
if (!this.props.mlAvailable || !this.props.hasMLJob) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { serviceName, transactionType } = this.props.urlParams;
|
|
||||||
|
|
||||||
if (!serviceName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<ShiftedEuiText size="xs">
|
|
||||||
<ShiftedIconWrapper>
|
|
||||||
<EuiIconTip
|
|
||||||
content={i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.machineLearningTooltip',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores >= 75.'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</ShiftedIconWrapper>
|
|
||||||
<span>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.machineLearningLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Machine learning:'
|
|
||||||
}
|
|
||||||
)}{' '}
|
|
||||||
</span>
|
|
||||||
<MLJobLink
|
|
||||||
serviceName={serviceName}
|
|
||||||
transactionType={transactionType}
|
|
||||||
location={this.props.location}
|
|
||||||
>
|
|
||||||
View Job
|
|
||||||
</MLJobLink>
|
|
||||||
</ShiftedEuiText>
|
|
||||||
</EuiFlexItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {
|
|
||||||
charts,
|
|
||||||
urlParams,
|
|
||||||
chartWrapper: ChartWrapper = React.Fragment
|
|
||||||
} = this.props;
|
|
||||||
const { noHits, responseTimeSeries, tpmSeries } = charts;
|
|
||||||
const { transactionType } = urlParams;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SyncChartGroup
|
|
||||||
render={hoverXHandlers => (
|
|
||||||
<EuiFlexGrid columns={2}>
|
|
||||||
<EuiFlexItem>
|
|
||||||
<EuiPanel>
|
|
||||||
<ChartWrapper>
|
|
||||||
<EuiFlexGroup justifyContent="spaceBetween">
|
|
||||||
<EuiFlexItem>
|
|
||||||
<EuiTitle size="xs">
|
|
||||||
<span>{responseTimeLabel(transactionType)}</span>
|
|
||||||
</EuiTitle>
|
|
||||||
</EuiFlexItem>
|
|
||||||
{this.renderMLHeader()}
|
|
||||||
</EuiFlexGroup>
|
|
||||||
<CustomPlot
|
|
||||||
noHits={noHits}
|
|
||||||
series={responseTimeSeries}
|
|
||||||
{...hoverXHandlers}
|
|
||||||
tickFormatY={this.getResponseTimeTickFormatter}
|
|
||||||
formatTooltipValue={this.getResponseTimeTooltipFormatter}
|
|
||||||
/>
|
|
||||||
</ChartWrapper>
|
|
||||||
</EuiPanel>
|
|
||||||
</EuiFlexItem>
|
|
||||||
|
|
||||||
<EuiFlexItem style={{ flexShrink: 1 }}>
|
|
||||||
<EuiPanel>
|
|
||||||
<ChartWrapper>
|
|
||||||
<EuiTitle size="xs">
|
|
||||||
<span>{tpmLabel(transactionType)}</span>
|
|
||||||
</EuiTitle>
|
|
||||||
<CustomPlot
|
|
||||||
noHits={noHits}
|
|
||||||
series={tpmSeries}
|
|
||||||
{...hoverXHandlers}
|
|
||||||
tickFormatY={this.getTPMFormatter}
|
|
||||||
formatTooltipValue={this.getTPMTooltipFormatter}
|
|
||||||
truncateLegends
|
|
||||||
/>
|
|
||||||
</ChartWrapper>
|
|
||||||
</EuiPanel>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGrid>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tpmLabel(type?: string) {
|
|
||||||
return type === 'request'
|
|
||||||
? i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.requestsPerMinuteLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Requests per minute'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Transactions per minute'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function responseTimeLabel(type?: string) {
|
|
||||||
switch (type) {
|
|
||||||
case 'page-load':
|
|
||||||
return i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.pageLoadTimesLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Page load times'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
case 'route-change':
|
|
||||||
return i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.routeChangeTimesLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Route change times'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return i18n.translate(
|
|
||||||
'xpack.apm.metrics.transactionChart.transactionDurationLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Transaction duration'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
121
x-pack/plugins/apm/public/hooks/useFetcher.integration.test.tsx
Normal file
121
x-pack/plugins/apm/public/hooks/useFetcher.integration.test.tsx
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { render } from 'react-testing-library';
|
||||||
|
import { delay, tick } from '../utils/testHelpers';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
|
||||||
|
/* tslint:disable:no-console */
|
||||||
|
const originalError = console.error;
|
||||||
|
beforeAll(() => {
|
||||||
|
console.error = jest.fn();
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
console.error = originalError;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function asyncFn(name: string, ms: number) {
|
||||||
|
await delay(ms);
|
||||||
|
return `Hello from ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('when simulating race condition', () => {
|
||||||
|
let requestCallOrder: Array<[string, string, number]>;
|
||||||
|
let renderSpy: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest
|
||||||
|
.spyOn(window, 'requestAnimationFrame')
|
||||||
|
.mockImplementation(cb => cb(0) as any);
|
||||||
|
|
||||||
|
renderSpy = jest.fn();
|
||||||
|
requestCallOrder = [];
|
||||||
|
|
||||||
|
function MyComponent({
|
||||||
|
name,
|
||||||
|
ms,
|
||||||
|
renderFn
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
ms: number;
|
||||||
|
renderFn: any;
|
||||||
|
}) {
|
||||||
|
const { data, status, error } = useFetcher(
|
||||||
|
async () => {
|
||||||
|
requestCallOrder.push(['request', name, ms]);
|
||||||
|
const res = await asyncFn(name, ms);
|
||||||
|
requestCallOrder.push(['response', name, ms]);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
[name, ms]
|
||||||
|
);
|
||||||
|
renderFn({ data, status, error });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<MyComponent name="John" ms={500} renderFn={renderSpy} />
|
||||||
|
);
|
||||||
|
|
||||||
|
rerender(<MyComponent name="Peter" ms={100} renderFn={renderSpy} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render initially render loading state', async () => {
|
||||||
|
expect(renderSpy).lastCalledWith({
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
status: 'loading'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render "Hello from Peter" after 200ms', async () => {
|
||||||
|
jest.advanceTimersByTime(200);
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
expect(renderSpy).lastCalledWith({
|
||||||
|
data: 'Hello from Peter',
|
||||||
|
error: undefined,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render "Hello from Peter" after 600ms', async () => {
|
||||||
|
jest.advanceTimersByTime(600);
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
expect(renderSpy).lastCalledWith({
|
||||||
|
data: 'Hello from Peter',
|
||||||
|
error: undefined,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should should NOT have rendered "Hello from John" at any point', async () => {
|
||||||
|
jest.advanceTimersByTime(600);
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
expect(renderSpy).not.toHaveBeenCalledWith({
|
||||||
|
data: 'Hello from John',
|
||||||
|
error: undefined,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send and receive calls in the right order', async () => {
|
||||||
|
jest.advanceTimersByTime(600);
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
expect(requestCallOrder).toEqual([
|
||||||
|
['request', 'John', 500],
|
||||||
|
['request', 'Peter', 100],
|
||||||
|
['response', 'Peter', 100],
|
||||||
|
['response', 'John', 500]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
159
x-pack/plugins/apm/public/hooks/useFetcher.test.tsx
Normal file
159
x-pack/plugins/apm/public/hooks/useFetcher.test.tsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* 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 { cleanup, renderHook } from 'react-hooks-testing-library';
|
||||||
|
import { delay } from '../utils/testHelpers';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
|
||||||
|
/* tslint:disable:no-console */
|
||||||
|
const originalError = console.error;
|
||||||
|
beforeAll(() => {
|
||||||
|
console.error = jest.fn();
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
console.error = originalError;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useFetcher', () => {
|
||||||
|
describe('when resolving after 500ms', () => {
|
||||||
|
let hook: ReturnType<typeof renderHook>;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
async function fn() {
|
||||||
|
await delay(500);
|
||||||
|
return 'response from hook';
|
||||||
|
}
|
||||||
|
hook = renderHook(() => useFetcher(() => fn(), []));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initially be empty', async () => {
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
status: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show loading spinner after 100ms', async () => {
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
await hook.waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
status: 'loading'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show success after 1 second', async () => {
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await hook.waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: 'response from hook',
|
||||||
|
error: undefined,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when throwing after 500ms', () => {
|
||||||
|
let hook: ReturnType<typeof renderHook>;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
async function fn() {
|
||||||
|
await delay(500);
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
}
|
||||||
|
hook = renderHook(() => useFetcher(() => fn(), []));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initially be empty', async () => {
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
status: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show loading spinner after 100ms', async () => {
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
await hook.waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
status: 'loading'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error after 1 second', async () => {
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await hook.waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: undefined,
|
||||||
|
error: expect.any(Error),
|
||||||
|
status: 'failure'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a hook already has data', () => {
|
||||||
|
it('should show "first response" while loading "second response"', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
const hook = renderHook(
|
||||||
|
({ callback, args }) => useFetcher(callback, args),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
callback: async () => 'first response',
|
||||||
|
args: ['a']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await hook.waitForNextUpdate();
|
||||||
|
|
||||||
|
// assert: first response has loaded and should be rendered
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: 'first response',
|
||||||
|
error: undefined,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// act: re-render hook with async callback
|
||||||
|
hook.rerender({
|
||||||
|
callback: async () => {
|
||||||
|
await delay(500);
|
||||||
|
return 'second response';
|
||||||
|
},
|
||||||
|
args: ['b']
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
await hook.waitForNextUpdate();
|
||||||
|
|
||||||
|
// assert: while loading new data the previous data should still be rendered
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: 'first response',
|
||||||
|
error: undefined,
|
||||||
|
status: 'loading'
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(500);
|
||||||
|
await hook.waitForNextUpdate();
|
||||||
|
|
||||||
|
// assert: "second response" has loaded and should be rendered
|
||||||
|
expect(hook.result.current).toEqual({
|
||||||
|
data: 'second response',
|
||||||
|
error: undefined,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
83
x-pack/plugins/apm/public/hooks/useFetcher.tsx
Normal file
83
x-pack/plugins/apm/public/hooks/useFetcher.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* 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 { useContext, useEffect, useState } from 'react';
|
||||||
|
import { GlobalFetchContext } from '../components/app/Main/GlobalFetchIndicator';
|
||||||
|
|
||||||
|
export enum FETCH_STATUS {
|
||||||
|
LOADING = 'loading',
|
||||||
|
SUCCESS = 'success',
|
||||||
|
FAILURE = 'failure'
|
||||||
|
}
|
||||||
|
|
||||||
|
// use this in request methods to signal to `useFetch` that all arguments are not yet available
|
||||||
|
export class MissingArgumentsError extends Error {}
|
||||||
|
|
||||||
|
export function useFetcher<Response>(
|
||||||
|
fn: () => Promise<Response>,
|
||||||
|
useEffectKey: Array<string | boolean | number | undefined>
|
||||||
|
) {
|
||||||
|
const { dispatchStatus } = useContext(GlobalFetchContext);
|
||||||
|
const [result, setResult] = useState<{
|
||||||
|
data?: Response;
|
||||||
|
status?: FETCH_STATUS;
|
||||||
|
error?: Error;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let didCancel = false;
|
||||||
|
let didFinish = false;
|
||||||
|
|
||||||
|
// only apply the loading indicator if the promise did not resolve immediately
|
||||||
|
// the promise will resolve immediately if the value was found in cache
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!didFinish && !didCancel) {
|
||||||
|
dispatchStatus({ name: fn.name, isLoading: true });
|
||||||
|
setResult({
|
||||||
|
data: result.data, // preserve data from previous state while loading next state
|
||||||
|
status: FETCH_STATUS.LOADING,
|
||||||
|
error: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doFetch() {
|
||||||
|
try {
|
||||||
|
const data = await fn();
|
||||||
|
if (!didCancel) {
|
||||||
|
dispatchStatus({ name: fn.name, isLoading: false });
|
||||||
|
setResult({
|
||||||
|
data,
|
||||||
|
status: FETCH_STATUS.SUCCESS,
|
||||||
|
error: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof MissingArgumentsError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!didCancel) {
|
||||||
|
dispatchStatus({ name: fn.name, isLoading: false });
|
||||||
|
setResult({
|
||||||
|
data: undefined,
|
||||||
|
status: FETCH_STATUS.FAILURE,
|
||||||
|
error: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
didFinish = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
doFetch();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatchStatus({ name: fn.name, isLoading: false });
|
||||||
|
didCancel = true;
|
||||||
|
};
|
||||||
|
}, useEffectKey);
|
||||||
|
|
||||||
|
return result || {};
|
||||||
|
}
|
85
x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
Normal file
85
x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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 { MetricsChartAPIResponse } from '../../server/lib/metrics/get_all_metrics_chart_data';
|
||||||
|
import { MemoryChartAPIResponse } from '../../server/lib/metrics/get_memory_chart_data';
|
||||||
|
import { loadMetricsChartDataForService } from '../services/rest/apm/metrics';
|
||||||
|
import {
|
||||||
|
getCPUSeries,
|
||||||
|
getMemorySeries
|
||||||
|
} from '../store/selectors/chartSelectors';
|
||||||
|
import { IUrlParams } from '../store/urlParams';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
const memory: MemoryChartAPIResponse = {
|
||||||
|
series: {
|
||||||
|
memoryUsedAvg: [],
|
||||||
|
memoryUsedMax: []
|
||||||
|
},
|
||||||
|
overallValues: {
|
||||||
|
memoryUsedAvg: null,
|
||||||
|
memoryUsedMax: null
|
||||||
|
},
|
||||||
|
totalHits: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_DATA: MetricsChartAPIResponse = {
|
||||||
|
memory,
|
||||||
|
cpu: {
|
||||||
|
series: {
|
||||||
|
systemCPUAverage: [],
|
||||||
|
systemCPUMax: [],
|
||||||
|
processCPUAverage: [],
|
||||||
|
processCPUMax: []
|
||||||
|
},
|
||||||
|
overallValues: {
|
||||||
|
systemCPUAverage: null,
|
||||||
|
systemCPUMax: null,
|
||||||
|
processCPUAverage: null,
|
||||||
|
processCPUMax: null
|
||||||
|
},
|
||||||
|
totalHits: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useServiceMetricCharts(urlParams: IUrlParams) {
|
||||||
|
const {
|
||||||
|
serviceName,
|
||||||
|
transactionType,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transactionName,
|
||||||
|
kuery
|
||||||
|
} = urlParams;
|
||||||
|
|
||||||
|
const { data = INITIAL_DATA, error, status } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadMetricsChartDataForService({
|
||||||
|
serviceName,
|
||||||
|
transactionName,
|
||||||
|
transactionType,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
kuery
|
||||||
|
}),
|
||||||
|
[serviceName, transactionName, transactionType, start, end, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedData = useMemo(
|
||||||
|
() => ({
|
||||||
|
memory: getMemorySeries(urlParams, data.memory),
|
||||||
|
cpu: getCPUSeries(data.cpu)
|
||||||
|
}),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: memoizedData,
|
||||||
|
status,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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 { loadTransactionDetailsCharts } from '../services/rest/apm/transaction_groups';
|
||||||
|
import { getTransactionCharts } from '../store/selectors/chartSelectors';
|
||||||
|
import { IUrlParams } from '../store/urlParams';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
export function useTransactionDetailsCharts(urlParams: IUrlParams) {
|
||||||
|
const {
|
||||||
|
serviceName,
|
||||||
|
transactionType,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transactionName,
|
||||||
|
kuery
|
||||||
|
} = urlParams;
|
||||||
|
|
||||||
|
const { data, error, status } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadTransactionDetailsCharts({
|
||||||
|
serviceName,
|
||||||
|
transactionName,
|
||||||
|
transactionType,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
kuery
|
||||||
|
}),
|
||||||
|
[serviceName, transactionName, transactionType, start, end, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedData = useMemo(() => getTransactionCharts(urlParams, data), [
|
||||||
|
data
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: memoizedData,
|
||||||
|
status,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 { loadTransactionDistribution } from '../services/rest/apm/transaction_groups';
|
||||||
|
import { IUrlParams } from '../store/urlParams';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
const INITIAL_DATA = { buckets: [], totalHits: 0, bucketSize: 0 };
|
||||||
|
|
||||||
|
export function useTransactionDistribution(urlParams: IUrlParams) {
|
||||||
|
const {
|
||||||
|
serviceName,
|
||||||
|
transactionType,
|
||||||
|
transactionId,
|
||||||
|
traceId,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transactionName,
|
||||||
|
kuery
|
||||||
|
} = urlParams;
|
||||||
|
|
||||||
|
const { data = INITIAL_DATA, status, error } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadTransactionDistribution({
|
||||||
|
serviceName,
|
||||||
|
transactionType,
|
||||||
|
transactionId,
|
||||||
|
traceId,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transactionName,
|
||||||
|
kuery
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
serviceName,
|
||||||
|
transactionType,
|
||||||
|
transactionId,
|
||||||
|
traceId,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transactionName,
|
||||||
|
kuery
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { data, status, error };
|
||||||
|
}
|
50
x-pack/plugins/apm/public/hooks/useTransactionList.ts
Normal file
50
x-pack/plugins/apm/public/hooks/useTransactionList.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 { TransactionListAPIResponse } from '../../server/lib/transactions/get_top_transactions';
|
||||||
|
import { loadTransactionList } from '../services/rest/apm/transaction_groups';
|
||||||
|
import { IUrlParams } from '../store/urlParams';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
const getRelativeImpact = (
|
||||||
|
impact: number,
|
||||||
|
impactMin: number,
|
||||||
|
impactMax: number
|
||||||
|
) =>
|
||||||
|
Math.max(
|
||||||
|
((impact - impactMin) / Math.max(impactMax - impactMin, 1)) * 100,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
function getWithRelativeImpact(items: TransactionListAPIResponse) {
|
||||||
|
const impacts = items.map(({ impact }) => impact);
|
||||||
|
const impactMin = Math.min(...impacts);
|
||||||
|
const impactMax = Math.max(...impacts);
|
||||||
|
|
||||||
|
return items.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
impactRelative: getRelativeImpact(item.impact, impactMin, impactMax)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTransactionList(urlParams: IUrlParams) {
|
||||||
|
const { serviceName, transactionType, start, end, kuery } = urlParams;
|
||||||
|
const { data = [], error, status } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadTransactionList({ serviceName, start, end, transactionType, kuery }),
|
||||||
|
[serviceName, start, end, transactionType, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedData = useMemo(() => getWithRelativeImpact(data), [data]);
|
||||||
|
return {
|
||||||
|
data: memoizedData,
|
||||||
|
status,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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 { loadTransactionOverviewCharts } from '../services/rest/apm/transaction_groups';
|
||||||
|
import { getTransactionCharts } from '../store/selectors/chartSelectors';
|
||||||
|
import { IUrlParams } from '../store/urlParams';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
export function useTransactionOverviewCharts(urlParams: IUrlParams) {
|
||||||
|
const {
|
||||||
|
serviceName,
|
||||||
|
transactionType,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
|
||||||
|
kuery
|
||||||
|
} = urlParams;
|
||||||
|
|
||||||
|
const { data, error, status } = useFetcher(
|
||||||
|
() =>
|
||||||
|
loadTransactionOverviewCharts({
|
||||||
|
serviceName,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transactionType,
|
||||||
|
kuery
|
||||||
|
}),
|
||||||
|
[serviceName, start, end, transactionType, kuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedData = useMemo(() => getTransactionCharts(urlParams, data), [
|
||||||
|
data
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: memoizedData,
|
||||||
|
status,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
28
x-pack/plugins/apm/public/hooks/useWaterfall.ts
Normal file
28
x-pack/plugins/apm/public/hooks/useWaterfall.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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 { getWaterfall } from '../components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||||
|
import { loadTrace } from '../services/rest/apm/traces';
|
||||||
|
import { IUrlParams } from '../store/urlParams';
|
||||||
|
import { useFetcher } from './useFetcher';
|
||||||
|
|
||||||
|
const INITIAL_DATA = { trace: [], errorsPerTransaction: {} };
|
||||||
|
|
||||||
|
export function useWaterfall(urlParams: IUrlParams) {
|
||||||
|
const { traceId, start, end, transactionId } = urlParams;
|
||||||
|
const { data = INITIAL_DATA, status, error } = useFetcher(
|
||||||
|
() => loadTrace({ traceId, start, end }),
|
||||||
|
[traceId, start, end]
|
||||||
|
);
|
||||||
|
|
||||||
|
const waterfall = useMemo(() => getWaterfall(data, transactionId), [
|
||||||
|
data,
|
||||||
|
transactionId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { data: waterfall, status, error };
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
|
@ -18,7 +18,6 @@ import { uiModules } from 'ui/modules';
|
||||||
import 'uiExports/autocompleteProviders';
|
import 'uiExports/autocompleteProviders';
|
||||||
import { GlobalHelpExtension } from './components/app/GlobalHelpExtension';
|
import { GlobalHelpExtension } from './components/app/GlobalHelpExtension';
|
||||||
import { Main } from './components/app/Main';
|
import { Main } from './components/app/Main';
|
||||||
import { GlobalProgress } from './components/app/Main/GlobalProgress';
|
|
||||||
import { history } from './components/shared/Links/url_helpers';
|
import { history } from './components/shared/Links/url_helpers';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import configureStore from './store/config/configureStore';
|
import configureStore from './store/config/configureStore';
|
||||||
|
@ -53,12 +52,9 @@ waitForRoot.then(() => {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<I18nContext>
|
<I18nContext>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Fragment>
|
|
||||||
<GlobalProgress />
|
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Main />
|
<Main />
|
||||||
</Router>
|
</Router>
|
||||||
</Fragment>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
</I18nContext>,
|
</I18nContext>,
|
||||||
document.getElementById(REACT_APP_ROOT_ID)
|
document.getElementById(REACT_APP_ROOT_ID)
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as kfetchModule from 'ui/kfetch';
|
import * as kfetchModule from 'ui/kfetch';
|
||||||
import { callApi } from '../rest/callApi';
|
import { mockNow } from '../../utils/testHelpers';
|
||||||
|
import { _clearCache, callApi } from '../rest/callApi';
|
||||||
import { SessionStorageMock } from './SessionStorageMock';
|
import { SessionStorageMock } from './SessionStorageMock';
|
||||||
|
|
||||||
describe('callApi', () => {
|
describe('callApi', () => {
|
||||||
|
@ -21,9 +22,9 @@ describe('callApi', () => {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
kfetchSpy.mockClear();
|
kfetchSpy.mockClear();
|
||||||
|
_clearCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('callApi', () => {
|
|
||||||
describe('apm_debug', () => {
|
describe('apm_debug', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sessionStorage.setItem('apm_debug', 'true');
|
sessionStorage.setItem('apm_debug', 'true');
|
||||||
|
@ -58,5 +59,112 @@ describe('callApi', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cache', () => {
|
||||||
|
let nowSpy: jest.Mock;
|
||||||
|
beforeEach(() => {
|
||||||
|
nowSpy = mockNow('2019');
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nowSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
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' } });
|
||||||
|
|
||||||
|
expect(kfetchSpy).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the call contains start/end params', () => {
|
||||||
|
it('should return cached response for identical calls', async () => {
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2010', end: '2011' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2010', end: '2011' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2010', end: '2011' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(kfetchSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return cached response for subsequent calls if arguments change', async () => {
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2010', end: '2011', foo: 'bar1' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2010', end: '2011', foo: 'bar2' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2010', end: '2011', foo: 'bar3' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(kfetchSpy).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return cached response if `end` is a future timestamp', async () => {
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { end: '2030' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { end: '2030' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { end: '2030' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(kfetchSpy).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached response if calls contain `end` param in the past', async () => {
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2009', end: '2010' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2009', end: '2010' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2009', end: '2010' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(kfetchSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached response even if order of properties change', async () => {
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { end: '2010', start: '2009' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
pathname: `/api/kibana`,
|
||||||
|
query: { start: '2009', end: '2010' }
|
||||||
|
});
|
||||||
|
await callApi({
|
||||||
|
query: { start: '2009', end: '2010' },
|
||||||
|
pathname: `/api/kibana`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(kfetchSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,29 +7,27 @@
|
||||||
import { ErrorDistributionAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/distribution/get_distribution';
|
import { ErrorDistributionAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/distribution/get_distribution';
|
||||||
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
||||||
import { ErrorGroupListAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_groups';
|
import { ErrorGroupListAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_groups';
|
||||||
|
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||||
import { IUrlParams } from '../../../store/urlParams';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { callApi } from '../callApi';
|
import { callApi } from '../callApi';
|
||||||
import { getEncodedEsQuery } from './apm';
|
import { getEncodedEsQuery } from './apm';
|
||||||
|
|
||||||
interface ErrorGroupListParams extends IUrlParams {
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadErrorGroupList({
|
export async function loadErrorGroupList({
|
||||||
serviceName,
|
serviceName,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
kuery,
|
kuery,
|
||||||
size,
|
|
||||||
sortField,
|
sortField,
|
||||||
sortDirection
|
sortDirection
|
||||||
}: ErrorGroupListParams) {
|
}: IUrlParams) {
|
||||||
|
if (!(serviceName && start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
return callApi<ErrorGroupListAPIResponse>({
|
return callApi<ErrorGroupListAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}/errors`,
|
pathname: `/api/apm/services/${serviceName}/errors`,
|
||||||
query: {
|
query: {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
size,
|
|
||||||
sortField,
|
sortField,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
esFilterQuery: await getEncodedEsQuery(kuery)
|
esFilterQuery: await getEncodedEsQuery(kuery)
|
||||||
|
@ -44,6 +42,9 @@ export async function loadErrorGroupDetails({
|
||||||
kuery,
|
kuery,
|
||||||
errorGroupId
|
errorGroupId
|
||||||
}: IUrlParams) {
|
}: IUrlParams) {
|
||||||
|
if (!(serviceName && start && end && errorGroupId)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
return callApi<ErrorGroupAPIResponse>({
|
return callApi<ErrorGroupAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}`,
|
pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -61,6 +62,10 @@ export async function loadErrorDistribution({
|
||||||
kuery,
|
kuery,
|
||||||
errorGroupId
|
errorGroupId
|
||||||
}: IUrlParams) {
|
}: IUrlParams) {
|
||||||
|
if (!(serviceName && start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
const pathname = errorGroupId
|
const pathname = errorGroupId
|
||||||
? `/api/apm/services/${serviceName}/errors/${errorGroupId}/distribution`
|
? `/api/apm/services/${serviceName}/errors/${errorGroupId}/distribution`
|
||||||
: `/api/apm/services/${serviceName}/errors/distribution`;
|
: `/api/apm/services/${serviceName}/errors/distribution`;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MetricsChartAPIResponse } from 'x-pack/plugins/apm/server/lib/metrics/get_all_metrics_chart_data';
|
||||||
import { IUrlParams } from '../../../store/urlParams';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { callApi } from '../callApi';
|
import { callApi } from '../callApi';
|
||||||
import { getEncodedEsQuery } from './apm';
|
import { getEncodedEsQuery } from './apm';
|
||||||
|
@ -14,7 +15,7 @@ export async function loadMetricsChartDataForService({
|
||||||
end,
|
end,
|
||||||
kuery
|
kuery
|
||||||
}: IUrlParams) {
|
}: IUrlParams) {
|
||||||
return callApi({
|
return callApi<MetricsChartAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}/metrics/charts`,
|
pathname: `/api/apm/services/${serviceName}/metrics/charts`,
|
||||||
query: {
|
query: {
|
||||||
start,
|
start,
|
||||||
|
|
|
@ -6,11 +6,16 @@
|
||||||
|
|
||||||
import { ServiceAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_service';
|
import { ServiceAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_service';
|
||||||
import { ServiceListAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_services';
|
import { ServiceListAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_services';
|
||||||
|
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||||
import { IUrlParams } from '../../../store/urlParams';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { callApi } from '../callApi';
|
import { callApi } from '../callApi';
|
||||||
import { getEncodedEsQuery } from './apm';
|
import { getEncodedEsQuery } from './apm';
|
||||||
|
|
||||||
export async function loadServiceList({ start, end, kuery }: IUrlParams) {
|
export async function loadServiceList({ start, end, kuery }: IUrlParams) {
|
||||||
|
if (!(start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
return callApi<ServiceListAPIResponse>({
|
return callApi<ServiceListAPIResponse>({
|
||||||
pathname: `/api/apm/services`,
|
pathname: `/api/apm/services`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -27,6 +32,10 @@ export async function loadServiceDetails({
|
||||||
end,
|
end,
|
||||||
kuery
|
kuery
|
||||||
}: IUrlParams) {
|
}: IUrlParams) {
|
||||||
|
if (!(serviceName && start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
return callApi<ServiceAPIResponse>({
|
return callApi<ServiceAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}`,
|
pathname: `/api/apm/services/${serviceName}`,
|
||||||
query: {
|
query: {
|
||||||
|
|
|
@ -13,7 +13,9 @@ export async function loadServerStatus() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAgentStatus() {
|
export async function loadAgentStatus() {
|
||||||
return callApi<{ dataFound: boolean }>({
|
const res = await callApi<{ dataFound: boolean }>({
|
||||||
pathname: `/api/apm/status/agent`
|
pathname: `/api/apm/status/agent`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return res.dataFound;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,16 @@
|
||||||
|
|
||||||
import { TraceListAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_top_traces';
|
import { TraceListAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_top_traces';
|
||||||
import { TraceAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_trace';
|
import { TraceAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_trace';
|
||||||
|
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||||
import { IUrlParams } from '../../../store/urlParams';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { callApi } from '../callApi';
|
import { callApi } from '../callApi';
|
||||||
import { getEncodedEsQuery } from './apm';
|
import { getEncodedEsQuery } from './apm';
|
||||||
|
|
||||||
export async function loadTrace({ traceId, start, end }: IUrlParams) {
|
export async function loadTrace({ traceId, start, end }: IUrlParams) {
|
||||||
|
if (!(traceId && start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
return callApi<TraceAPIResponse>({
|
return callApi<TraceAPIResponse>({
|
||||||
pathname: `/api/apm/traces/${traceId}`,
|
pathname: `/api/apm/traces/${traceId}`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -21,6 +26,10 @@ export async function loadTrace({ traceId, start, end }: IUrlParams) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTraceList({ start, end, kuery }: IUrlParams) {
|
export async function loadTraceList({ start, end, kuery }: IUrlParams) {
|
||||||
|
if (!(start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
return callApi<TraceListAPIResponse>({
|
return callApi<TraceListAPIResponse>({
|
||||||
pathname: '/api/apm/traces',
|
pathname: '/api/apm/traces',
|
||||||
query: {
|
query: {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts';
|
import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts';
|
||||||
import { ITransactionDistributionAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution';
|
import { ITransactionDistributionAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution';
|
||||||
import { TransactionListAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_top_transactions';
|
import { TransactionListAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_top_transactions';
|
||||||
|
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||||
import { IUrlParams } from '../../../store/urlParams';
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
import { callApi } from '../callApi';
|
import { callApi } from '../callApi';
|
||||||
import { getEncodedEsQuery } from './apm';
|
import { getEncodedEsQuery } from './apm';
|
||||||
|
@ -16,8 +17,12 @@ export async function loadTransactionList({
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
kuery,
|
kuery,
|
||||||
transactionType = 'request'
|
transactionType
|
||||||
}: IUrlParams) {
|
}: IUrlParams) {
|
||||||
|
if (!(serviceName && transactionType && start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
return await callApi<TransactionListAPIResponse>({
|
return await callApi<TransactionListAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}/transaction_groups/${transactionType}`,
|
pathname: `/api/apm/services/${serviceName}/transaction_groups/${transactionType}`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -33,11 +38,15 @@ export async function loadTransactionDistribution({
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
transactionName,
|
transactionName,
|
||||||
transactionType = 'request',
|
transactionType,
|
||||||
transactionId,
|
transactionId,
|
||||||
traceId,
|
traceId,
|
||||||
kuery
|
kuery
|
||||||
}: Required<IUrlParams>) {
|
}: IUrlParams) {
|
||||||
|
if (!(serviceName && transactionName && transactionType && start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
return callApi<ITransactionDistributionAPIResponse>({
|
return callApi<ITransactionDistributionAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}/transaction_groups/${transactionType}/${encodeURIComponent(
|
pathname: `/api/apm/services/${serviceName}/transaction_groups/${transactionType}/${encodeURIComponent(
|
||||||
transactionName
|
transactionName
|
||||||
|
@ -52,14 +61,18 @@ export async function loadTransactionDistribution({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadDetailsCharts({
|
export async function loadTransactionDetailsCharts({
|
||||||
serviceName,
|
serviceName,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
kuery,
|
kuery,
|
||||||
transactionType = 'request',
|
transactionType,
|
||||||
transactionName
|
transactionName
|
||||||
}: Required<IUrlParams>) {
|
}: IUrlParams) {
|
||||||
|
if (!(serviceName && transactionName && transactionType && start && end)) {
|
||||||
|
throw new MissingArgumentsError();
|
||||||
|
}
|
||||||
|
|
||||||
return callApi<TimeSeriesAPIResponse>({
|
return callApi<TimeSeriesAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}/transaction_groups/${transactionType}/${encodeURIComponent(
|
pathname: `/api/apm/services/${serviceName}/transaction_groups/${transactionType}/${encodeURIComponent(
|
||||||
transactionName
|
transactionName
|
||||||
|
@ -72,31 +85,23 @@ export async function loadDetailsCharts({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadOverviewCharts({
|
export async function loadTransactionOverviewCharts({
|
||||||
serviceName,
|
serviceName,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
kuery,
|
kuery,
|
||||||
transactionType = 'request'
|
transactionType
|
||||||
}: IUrlParams) {
|
}: IUrlParams) {
|
||||||
return callApi<TimeSeriesAPIResponse>({
|
if (!(serviceName && start && end)) {
|
||||||
pathname: `/api/apm/services/${serviceName}/transaction_groups/${transactionType}/charts`,
|
throw new MissingArgumentsError();
|
||||||
query: {
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
esFilterQuery: await getEncodedEsQuery(kuery)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadOverviewChartsForAllTypes({
|
const pathname = transactionType
|
||||||
serviceName,
|
? `/api/apm/services/${serviceName}/transaction_groups/${transactionType}/charts`
|
||||||
start,
|
: `/api/apm/services/${serviceName}/transaction_groups/charts`;
|
||||||
end,
|
|
||||||
kuery
|
|
||||||
}: IUrlParams) {
|
|
||||||
return callApi<TimeSeriesAPIResponse>({
|
return callApi<TimeSeriesAPIResponse>({
|
||||||
pathname: `/api/apm/services/${serviceName}/transaction_groups/charts`,
|
pathname,
|
||||||
query: {
|
query: {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { startsWith } from 'lodash';
|
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 { kfetch, KFetchOptions } from 'ui/kfetch';
|
||||||
import { KFetchKibanaOptions } from 'ui/kfetch/kfetch';
|
import { KFetchKibanaOptions } from 'ui/kfetch/kfetch';
|
||||||
|
|
||||||
|
@ -26,10 +29,49 @@ function fetchOptionsWithDebug(fetchOptions: KFetchOptions) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cache = new LRU<string, any>({ max: 100, maxAge: 1000 * 60 * 60 });
|
||||||
|
|
||||||
|
export function _clearCache() {
|
||||||
|
cache.reset();
|
||||||
|
}
|
||||||
|
|
||||||
export async function callApi<T = void>(
|
export async function callApi<T = void>(
|
||||||
fetchOptions: KFetchOptions,
|
fetchOptions: KFetchOptions,
|
||||||
options?: KFetchKibanaOptions
|
options?: KFetchKibanaOptions
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const combinedFetchOptions = fetchOptionsWithDebug(fetchOptions);
|
const cacheKey = getCacheKey(fetchOptions);
|
||||||
return await kfetch(combinedFetchOptions, options);
|
const cacheResponse = cache.get(cacheKey);
|
||||||
|
if (cacheResponse) {
|
||||||
|
return cacheResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedFetchOptions = fetchOptionsWithDebug(fetchOptions);
|
||||||
|
const res = await kfetch(combinedFetchOptions, options);
|
||||||
|
|
||||||
|
if (isCachable(fetchOptions)) {
|
||||||
|
cache.set(cacheKey, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
if (
|
||||||
|
!(fetchOptions.query && fetchOptions.query.start && fetchOptions.query.end)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isString(fetchOptions.query.end) &&
|
||||||
|
new Date(fetchOptions.query.end).getTime() < Date.now()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// order the options object to make sure that two objects with the same arguments, produce produce the
|
||||||
|
// same cache key regardless of the order of properties
|
||||||
|
function getCacheKey(options: FetchOptions) {
|
||||||
|
return hash(options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ describe('root reducer', () => {
|
||||||
|
|
||||||
expect(state).toEqual({
|
expect(state).toEqual({
|
||||||
location: { hash: '', pathname: '', search: '' },
|
location: { hash: '', pathname: '', search: '' },
|
||||||
reactReduxRequest: {},
|
|
||||||
urlParams: {}
|
urlParams: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,31 +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 { createInitialDataSelector } from '../helpers';
|
|
||||||
|
|
||||||
describe('createInitialDataSelector', () => {
|
|
||||||
it('should use initialData when data is missing from state', () => {
|
|
||||||
const state = {};
|
|
||||||
const initialData = { foo: 'bar' };
|
|
||||||
const withInitialData = createInitialDataSelector(initialData);
|
|
||||||
|
|
||||||
expect(withInitialData(state)).toBe(withInitialData(state));
|
|
||||||
expect(withInitialData(state, initialData)).toEqual({
|
|
||||||
data: { foo: 'bar' }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use data when available', () => {
|
|
||||||
const state = { data: 'hello' };
|
|
||||||
const initialData = { foo: 'bar' };
|
|
||||||
const withInitialData = createInitialDataSelector(initialData);
|
|
||||||
|
|
||||||
expect(withInitialData(state)).toBe(withInitialData(state));
|
|
||||||
expect(withInitialData(state, initialData)).toEqual({
|
|
||||||
data: 'hello'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,71 +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 React from 'react';
|
|
||||||
import * as rest from '../../../services/rest/apm/services';
|
|
||||||
import { getServiceList, ServiceListRequest } from '../serviceList';
|
|
||||||
import { mountWithStore } from '../../../utils/testHelpers';
|
|
||||||
|
|
||||||
describe('serviceList', () => {
|
|
||||||
describe('getServiceList', () => {
|
|
||||||
it('should return default value when empty', () => {
|
|
||||||
const state = { reactReduxRequest: {}, sorting: { service: {} } };
|
|
||||||
expect(getServiceList(state)).toEqual({ data: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return serviceList when not empty', () => {
|
|
||||||
const state = {
|
|
||||||
reactReduxRequest: { serviceList: { data: [{ foo: 'bar' }] } },
|
|
||||||
sorting: { service: {} }
|
|
||||||
};
|
|
||||||
expect(getServiceList(state)).toEqual({ data: [{ foo: 'bar' }] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ServiceListRequest', () => {
|
|
||||||
let loadSpy;
|
|
||||||
let renderSpy;
|
|
||||||
let wrapper;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = {
|
|
||||||
reactReduxRequest: {
|
|
||||||
serviceList: { status: 'my-status', data: [{ foo: 'bar' }] }
|
|
||||||
},
|
|
||||||
sorting: { service: {} }
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSpy = jest.spyOn(rest, 'loadServiceList').mockReturnValue();
|
|
||||||
renderSpy = jest.fn().mockReturnValue(<div>rendered</div>);
|
|
||||||
|
|
||||||
wrapper = mountWithStore(
|
|
||||||
<ServiceListRequest
|
|
||||||
urlParams={{ start: 'myStart', end: 'myEnd' }}
|
|
||||||
render={renderSpy}
|
|
||||||
/>,
|
|
||||||
state
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call render method', () => {
|
|
||||||
expect(renderSpy).toHaveBeenCalledWith({
|
|
||||||
data: [{ foo: 'bar' }],
|
|
||||||
status: 'my-status'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call "loadServiceList"', () => {
|
|
||||||
expect(loadSpy).toHaveBeenCalledWith({
|
|
||||||
start: 'myStart',
|
|
||||||
end: 'myEnd'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render component', () => {
|
|
||||||
expect(wrapper.html()).toEqual('<div>rendered</div>');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,49 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { ErrorDistributionAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/distribution/get_distribution';
|
|
||||||
import { loadErrorDistribution } from '../../services/rest/apm/error_groups';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'errorDistribution';
|
|
||||||
const INITIAL_DATA: ErrorDistributionAPIResponse = {
|
|
||||||
buckets: [],
|
|
||||||
totalHits: 0,
|
|
||||||
bucketSize: 0
|
|
||||||
};
|
|
||||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
|
||||||
|
|
||||||
export function getErrorDistribution(state: IReduxState) {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorDistributionRequest({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ErrorDistributionAPIResponse>;
|
|
||||||
}) {
|
|
||||||
const { serviceName, start, end, errorGroupId, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadErrorDistribution}
|
|
||||||
args={[{ serviceName, start, end, errorGroupId, kuery }]}
|
|
||||||
selector={getErrorDistribution}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,45 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
|
||||||
import { loadErrorGroupDetails } from '../../services/rest/apm/error_groups';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'errorGroupDetails';
|
|
||||||
const INITIAL_DATA: ErrorGroupAPIResponse = { occurrencesCount: 0 };
|
|
||||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
|
||||||
|
|
||||||
export function getErrorGroupDetails(state: IReduxState) {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorGroupDetailsRequest({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ErrorGroupAPIResponse>;
|
|
||||||
}) {
|
|
||||||
const { serviceName, errorGroupId, start, end, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end && errorGroupId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadErrorGroupDetails}
|
|
||||||
args={[{ serviceName, start, end, errorGroupId, kuery }]}
|
|
||||||
selector={getErrorGroupDetails}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,52 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { ErrorGroupListAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_groups';
|
|
||||||
import { loadErrorGroupList } from '../../services/rest/apm/error_groups';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'errorGroupList';
|
|
||||||
const INITIAL_DATA: ErrorGroupListAPIResponse = [];
|
|
||||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
|
||||||
|
|
||||||
export function getErrorGroupList(state: IReduxState) {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorGroupOverviewRequest({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ErrorGroupListAPIResponse>;
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
serviceName,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
sortField,
|
|
||||||
sortDirection,
|
|
||||||
kuery
|
|
||||||
} = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadErrorGroupList}
|
|
||||||
args={[{ serviceName, start, end, sortField, sortDirection, kuery }]}
|
|
||||||
selector={getErrorGroupList}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,18 +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 { get } from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
export function createInitialDataSelector<T>(initialData: T) {
|
|
||||||
return createSelector(
|
|
||||||
state => state,
|
|
||||||
state => {
|
|
||||||
const data: T = get(state, 'data') || initialData;
|
|
||||||
return { ...state, data };
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,35 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { LicenseApiResponse, loadLicense } from '../../services/rest/xpack';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'license';
|
|
||||||
const INITIAL_DATA = {
|
|
||||||
features: {
|
|
||||||
watcher: { is_available: false },
|
|
||||||
ml: { is_available: false }
|
|
||||||
},
|
|
||||||
license: { is_active: false }
|
|
||||||
};
|
|
||||||
|
|
||||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
|
||||||
|
|
||||||
export function getLicense(state: IReduxState) {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LicenceRequest({
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
render: RRRRender<LicenseApiResponse>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Request id={ID} fn={loadLicense} selector={getLicense} render={render} />
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,39 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { getMLJob, MLJobApiResponse } from '../../services/rest/ml';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const INITIAL_DATA = { count: 0, jobs: [] };
|
|
||||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
|
||||||
const ID = 'MLJobs';
|
|
||||||
|
|
||||||
function selectMlJobs(state: IReduxState) {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MLJobsRequest({
|
|
||||||
serviceName,
|
|
||||||
transactionType = '*',
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
serviceName: string;
|
|
||||||
transactionType?: string;
|
|
||||||
render: RRRRender<MLJobApiResponse>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={getMLJob}
|
|
||||||
args={[{ serviceName, transactionType }]}
|
|
||||||
render={render}
|
|
||||||
selector={selectMlJobs}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,51 +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 { first, get } from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
|
||||||
import { ServiceAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_service';
|
|
||||||
import { loadServiceDetails } from '../../services/rest/apm/services';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'serviceDetails';
|
|
||||||
const INITIAL_DATA = { types: [] };
|
|
||||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
|
||||||
|
|
||||||
export function getServiceDetails(state: IReduxState) {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultTransactionType(state: IReduxState) {
|
|
||||||
const types: string[] = get(state.reactReduxRequest[ID], 'data.types');
|
|
||||||
return first(types);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ServiceDetailsRequest({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ServiceAPIResponse>;
|
|
||||||
}) {
|
|
||||||
const { serviceName, start, end, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadServiceDetails}
|
|
||||||
args={[{ serviceName, start, end, kuery }]}
|
|
||||||
selector={getServiceDetails}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,49 +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 React from 'react';
|
|
||||||
import { Request, RRRRender, RRRRenderResponse } from 'react-redux-request';
|
|
||||||
import { ServiceListAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_services';
|
|
||||||
import { loadServiceList } from '../../services/rest/apm/services';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'serviceList';
|
|
||||||
const INITIAL_DATA: ServiceListAPIResponse = [];
|
|
||||||
const withInitialData = createInitialDataSelector<ServiceListAPIResponse>(
|
|
||||||
INITIAL_DATA
|
|
||||||
);
|
|
||||||
|
|
||||||
export function getServiceList(
|
|
||||||
state: IReduxState
|
|
||||||
): RRRRenderResponse<ServiceListAPIResponse> {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ServiceListRequest({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ServiceListAPIResponse>;
|
|
||||||
}) {
|
|
||||||
const { start, end, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!(start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadServiceList}
|
|
||||||
args={[{ start, end, kuery }]}
|
|
||||||
selector={getServiceList}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,92 +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 React from 'react';
|
|
||||||
import { Request, RRRRender, RRRRenderResponse } from 'react-redux-request';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { loadMetricsChartDataForService } from 'x-pack/plugins/apm/public/services/rest/apm/metrics';
|
|
||||||
import { IMemoryChartData } from 'x-pack/plugins/apm/public/store/selectors/chartSelectors';
|
|
||||||
import { MetricsChartAPIResponse } from 'x-pack/plugins/apm/server/lib/metrics/get_all_metrics_chart_data';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { getCPUSeries, getMemorySeries } from '../selectors/chartSelectors';
|
|
||||||
import { getUrlParams, IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'metricsChartData';
|
|
||||||
const INITIAL_DATA: MetricsChartAPIResponse = {
|
|
||||||
memory: {
|
|
||||||
series: {
|
|
||||||
memoryUsedAvg: [],
|
|
||||||
memoryUsedMax: []
|
|
||||||
},
|
|
||||||
overallValues: {
|
|
||||||
memoryUsedAvg: null,
|
|
||||||
memoryUsedMax: null
|
|
||||||
},
|
|
||||||
totalHits: 0
|
|
||||||
},
|
|
||||||
cpu: {
|
|
||||||
series: {
|
|
||||||
systemCPUAverage: [],
|
|
||||||
systemCPUMax: [],
|
|
||||||
processCPUAverage: [],
|
|
||||||
processCPUMax: []
|
|
||||||
},
|
|
||||||
overallValues: {
|
|
||||||
systemCPUAverage: null,
|
|
||||||
systemCPUMax: null,
|
|
||||||
processCPUAverage: null,
|
|
||||||
processCPUMax: null
|
|
||||||
},
|
|
||||||
totalHits: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type MetricsChartDataSelector = (
|
|
||||||
state: IReduxState
|
|
||||||
) => RRRRenderResponse<MetricsChartAPIResponse>;
|
|
||||||
|
|
||||||
const withInitialData = createInitialDataSelector<MetricsChartAPIResponse>(
|
|
||||||
INITIAL_DATA
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectMetricsChartData: MetricsChartDataSelector = state =>
|
|
||||||
withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
|
|
||||||
export const selectTransformedMetricsChartData = createSelector(
|
|
||||||
[getUrlParams, selectMetricsChartData],
|
|
||||||
(urlParams, response) => ({
|
|
||||||
...response,
|
|
||||||
data: {
|
|
||||||
...response.data,
|
|
||||||
memory: getMemorySeries(urlParams, response.data.memory),
|
|
||||||
cpu: getCPUSeries(response.data.cpu)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<IMemoryChartData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MetricsChartDataRequest({ urlParams, render }: Props) {
|
|
||||||
const { serviceName, start, end } = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadMetricsChartDataForService}
|
|
||||||
args={[urlParams]}
|
|
||||||
selector={selectTransformedMetricsChartData}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,56 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { TraceListAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_top_traces';
|
|
||||||
import { loadTraceList } from '../../services/rest/apm/traces';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'traceList';
|
|
||||||
const INITIAL_DATA: TraceListAPIResponse = [];
|
|
||||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
|
||||||
|
|
||||||
const selectRRR = (state = {} as IReduxState) => state.reactReduxRequest;
|
|
||||||
|
|
||||||
export const selectTraceList = createSelector(
|
|
||||||
[selectRRR],
|
|
||||||
reactReduxRequest => {
|
|
||||||
return withInitialData(reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<TraceListAPIResponse>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TraceListRequest({ urlParams, render }: Props) {
|
|
||||||
const { start, end, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!start || !end) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadTraceList}
|
|
||||||
args={[
|
|
||||||
{
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
kuery
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
selector={selectTraceList}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,73 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { ITransactionChartData } from 'x-pack/plugins/apm/public/store/selectors/chartSelectors';
|
|
||||||
import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts';
|
|
||||||
import { loadDetailsCharts } from '../../services/rest/apm/transaction_groups';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { getTransactionCharts } from '../selectors/chartSelectors';
|
|
||||||
import { getUrlParams, IUrlParams } from '../urlParams';
|
|
||||||
|
|
||||||
const ID = 'transactionDetailsCharts';
|
|
||||||
const INITIAL_DATA: TimeSeriesAPIResponse = {
|
|
||||||
apmTimeseries: {
|
|
||||||
totalHits: 0,
|
|
||||||
responseTimes: {
|
|
||||||
avg: [],
|
|
||||||
p95: [],
|
|
||||||
p99: []
|
|
||||||
},
|
|
||||||
tpmBuckets: [],
|
|
||||||
overallAvgDuration: undefined
|
|
||||||
},
|
|
||||||
anomalyTimeseries: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTransactionDetailsCharts = createSelector(
|
|
||||||
getUrlParams,
|
|
||||||
(state: IReduxState) => state.reactReduxRequest[ID],
|
|
||||||
(urlParams, detailCharts = {}) => {
|
|
||||||
return {
|
|
||||||
...detailCharts,
|
|
||||||
data: getTransactionCharts(urlParams, detailCharts.data || INITIAL_DATA)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ITransactionChartData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TransactionDetailsChartsRequest({ urlParams, render }: Props) {
|
|
||||||
const {
|
|
||||||
serviceName,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
transactionType,
|
|
||||||
transactionName,
|
|
||||||
kuery
|
|
||||||
} = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end && transactionType && transactionName)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadDetailsCharts}
|
|
||||||
args={[
|
|
||||||
{ serviceName, start, end, transactionType, transactionName, kuery }
|
|
||||||
]}
|
|
||||||
selector={getTransactionDetailsCharts}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,81 +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 React from 'react';
|
|
||||||
import { Request, RRRRender, RRRRenderResponse } from 'react-redux-request';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { ITransactionDistributionAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution';
|
|
||||||
import { loadTransactionDistribution } from '../../services/rest/apm/transaction_groups';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'transactionDistribution';
|
|
||||||
const INITIAL_DATA = { buckets: [], totalHits: 0, bucketSize: 0 };
|
|
||||||
const withInitialData = createInitialDataSelector<
|
|
||||||
ITransactionDistributionAPIResponse
|
|
||||||
>(INITIAL_DATA);
|
|
||||||
|
|
||||||
export function getTransactionDistribution(
|
|
||||||
state: IReduxState
|
|
||||||
): RRRRenderResponse<ITransactionDistributionAPIResponse> {
|
|
||||||
return withInitialData(state.reactReduxRequest[ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDefaultDistributionSample = createSelector(
|
|
||||||
getTransactionDistribution,
|
|
||||||
distribution => {
|
|
||||||
const { defaultSample = {} } = distribution.data;
|
|
||||||
return {
|
|
||||||
traceId: defaultSample.traceId,
|
|
||||||
transactionId: defaultSample.transactionId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function TransactionDistributionRequest({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ITransactionDistributionAPIResponse>;
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
serviceName,
|
|
||||||
transactionType,
|
|
||||||
transactionId,
|
|
||||||
traceId,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
transactionName,
|
|
||||||
kuery
|
|
||||||
} = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && transactionType && start && end && transactionName)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadTransactionDistribution}
|
|
||||||
args={[
|
|
||||||
{
|
|
||||||
serviceName,
|
|
||||||
transactionType,
|
|
||||||
transactionId,
|
|
||||||
traceId,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
transactionName,
|
|
||||||
kuery
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
selector={getTransactionDistribution}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,85 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { TransactionListAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_top_transactions';
|
|
||||||
import { loadTransactionList } from '../../services/rest/apm/transaction_groups';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
import { createInitialDataSelector } from './helpers';
|
|
||||||
|
|
||||||
const ID = 'transactionList';
|
|
||||||
const INITIAL_DATA: TransactionListAPIResponse = [];
|
|
||||||
const withInitialData = createInitialDataSelector<TransactionListAPIResponse>(
|
|
||||||
INITIAL_DATA
|
|
||||||
);
|
|
||||||
|
|
||||||
const getRelativeImpact = (
|
|
||||||
impact: number,
|
|
||||||
impactMin: number,
|
|
||||||
impactMax: number
|
|
||||||
) =>
|
|
||||||
Math.max(
|
|
||||||
((impact - impactMin) / Math.max(impactMax - impactMin, 1)) * 100,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
function getWithRelativeImpact(items: TransactionListAPIResponse) {
|
|
||||||
const impacts = items.map(({ impact }) => impact);
|
|
||||||
const impactMin = Math.min(...impacts);
|
|
||||||
const impactMax = Math.max(...impacts);
|
|
||||||
|
|
||||||
return items.map(item => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
impactRelative: getRelativeImpact(item.impact, impactMin, impactMax)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTransactionList = createSelector(
|
|
||||||
(state: IReduxState) => withInitialData(state.reactReduxRequest[ID]),
|
|
||||||
transactionList => {
|
|
||||||
return {
|
|
||||||
...transactionList,
|
|
||||||
data: getWithRelativeImpact(transactionList.data)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function TransactionListRequest({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<TransactionListAPIResponse>;
|
|
||||||
}) {
|
|
||||||
const { serviceName, start, end, transactionType, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadTransactionList}
|
|
||||||
args={[
|
|
||||||
{
|
|
||||||
serviceName,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
transactionType,
|
|
||||||
kuery
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
selector={getTransactionList}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,97 +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 { get } from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { ITransactionChartData } from 'x-pack/plugins/apm/public/store/selectors/chartSelectors';
|
|
||||||
import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts';
|
|
||||||
import {
|
|
||||||
loadOverviewCharts,
|
|
||||||
loadOverviewChartsForAllTypes
|
|
||||||
} from '../../services/rest/apm/transaction_groups';
|
|
||||||
import { IReduxState } from '../rootReducer';
|
|
||||||
import { getTransactionCharts } from '../selectors/chartSelectors';
|
|
||||||
import { getUrlParams, IUrlParams } from '../urlParams';
|
|
||||||
|
|
||||||
const ID = 'transactionOverviewCharts';
|
|
||||||
const INITIAL_DATA: TimeSeriesAPIResponse = {
|
|
||||||
apmTimeseries: {
|
|
||||||
totalHits: 0,
|
|
||||||
responseTimes: {
|
|
||||||
avg: [],
|
|
||||||
p95: [],
|
|
||||||
p99: []
|
|
||||||
},
|
|
||||||
tpmBuckets: [],
|
|
||||||
overallAvgDuration: undefined
|
|
||||||
},
|
|
||||||
anomalyTimeseries: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectChartData = (state: IReduxState) => state.reactReduxRequest[ID];
|
|
||||||
|
|
||||||
export const getTransactionOverviewCharts = createSelector(
|
|
||||||
[getUrlParams, selectChartData],
|
|
||||||
(urlParams, overviewCharts = {}) => {
|
|
||||||
return {
|
|
||||||
...overviewCharts,
|
|
||||||
data: getTransactionCharts(urlParams, overviewCharts.data || INITIAL_DATA)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectHasMLJob = createSelector(
|
|
||||||
[selectChartData],
|
|
||||||
chartData => get(chartData, 'data.anomalyTimeseries') !== undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
render: RRRRender<ITransactionChartData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TransactionOverviewChartsRequest({ urlParams, render }: Props) {
|
|
||||||
const { serviceName, start, end, transactionType, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadOverviewCharts}
|
|
||||||
args={[{ serviceName, start, end, transactionType, kuery }]}
|
|
||||||
selector={getTransactionOverviewCharts}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignores transaction type from urlParams and requests charts
|
|
||||||
// for ALL transaction types within this service
|
|
||||||
export function TransactionOverviewChartsRequestForAllTypes({
|
|
||||||
urlParams,
|
|
||||||
render
|
|
||||||
}: Props) {
|
|
||||||
const { serviceName, start, end, kuery } = urlParams;
|
|
||||||
|
|
||||||
if (!(serviceName && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request
|
|
||||||
id={ID}
|
|
||||||
fn={loadOverviewChartsForAllTypes}
|
|
||||||
args={[{ serviceName, start, end, kuery }]}
|
|
||||||
selector={getTransactionOverviewCharts}
|
|
||||||
render={render}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,51 +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 React from 'react';
|
|
||||||
import { Request, RRRRender } from 'react-redux-request';
|
|
||||||
import { TraceAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_trace';
|
|
||||||
import {
|
|
||||||
getWaterfall,
|
|
||||||
IWaterfall
|
|
||||||
} from '../../components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
|
||||||
import { loadTrace } from '../../services/rest/apm/traces';
|
|
||||||
import { IUrlParams } from '../urlParams';
|
|
||||||
|
|
||||||
export const ID = 'waterfall';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
urlParams: IUrlParams;
|
|
||||||
traceId?: string;
|
|
||||||
render: RRRRender<IWaterfall>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WaterfallRequest({ urlParams, render, traceId }: Props) {
|
|
||||||
const { start, end } = urlParams;
|
|
||||||
|
|
||||||
if (!(traceId && start && end)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Request<TraceAPIResponse>
|
|
||||||
id={ID}
|
|
||||||
fn={loadTrace}
|
|
||||||
args={[{ traceId, start, end }]}
|
|
||||||
render={({
|
|
||||||
args,
|
|
||||||
data = { trace: [], errorsPerTransaction: {} },
|
|
||||||
status
|
|
||||||
}) => {
|
|
||||||
const waterfall = getWaterfall(
|
|
||||||
data.trace,
|
|
||||||
data.errorsPerTransaction,
|
|
||||||
urlParams.transactionId
|
|
||||||
);
|
|
||||||
return render({ args, data: waterfall, status });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -5,20 +5,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import { reducer } from 'react-redux-request';
|
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { StringMap } from '../../typings/common';
|
|
||||||
import { locationReducer } from './location';
|
import { locationReducer } from './location';
|
||||||
import { IUrlParams, urlParamsReducer } from './urlParams';
|
import { IUrlParams, urlParamsReducer } from './urlParams';
|
||||||
|
|
||||||
export interface IReduxState {
|
export interface IReduxState {
|
||||||
location: Location;
|
location: Location;
|
||||||
urlParams: IUrlParams;
|
urlParams: IUrlParams;
|
||||||
reactReduxRequest: StringMap<any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
location: locationReducer,
|
location: locationReducer,
|
||||||
urlParams: urlParamsReducer,
|
urlParams: urlParamsReducer
|
||||||
reactReduxRequest: reducer
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -65,13 +65,27 @@ export interface ITransactionChartData {
|
||||||
noHits: boolean;
|
noHits: boolean;
|
||||||
tpmSeries: ITpmBucket[] | IEmptySeries[];
|
tpmSeries: ITpmBucket[] | IEmptySeries[];
|
||||||
responseTimeSeries: TimeSerie[] | IEmptySeries[];
|
responseTimeSeries: TimeSerie[] | IEmptySeries[];
|
||||||
|
hasMLJob: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const INITIAL_DATA = {
|
||||||
|
apmTimeseries: {
|
||||||
|
totalHits: 0,
|
||||||
|
responseTimes: {
|
||||||
|
avg: [],
|
||||||
|
p95: [],
|
||||||
|
p99: []
|
||||||
|
},
|
||||||
|
tpmBuckets: [],
|
||||||
|
overallAvgDuration: undefined
|
||||||
|
},
|
||||||
|
anomalyTimeseries: undefined
|
||||||
|
};
|
||||||
|
|
||||||
export function getTransactionCharts(
|
export function getTransactionCharts(
|
||||||
urlParams: IUrlParams,
|
{ start, end, transactionType }: IUrlParams,
|
||||||
timeseriesResponse: TimeSeriesAPIResponse
|
timeseriesResponse: TimeSeriesAPIResponse = INITIAL_DATA
|
||||||
) {
|
): ITransactionChartData {
|
||||||
const { start, end, transactionType } = urlParams;
|
|
||||||
const { apmTimeseries, anomalyTimeseries } = timeseriesResponse;
|
const { apmTimeseries, anomalyTimeseries } = timeseriesResponse;
|
||||||
const noHits = apmTimeseries.totalHits === 0;
|
const noHits = apmTimeseries.totalHits === 0;
|
||||||
const tpmSeries = noHits
|
const tpmSeries = noHits
|
||||||
|
@ -82,26 +96,22 @@ export function getTransactionCharts(
|
||||||
? getEmptySerie(start, end)
|
? getEmptySerie(start, end)
|
||||||
: getResponseTimeSeries(apmTimeseries, anomalyTimeseries);
|
: getResponseTimeSeries(apmTimeseries, anomalyTimeseries);
|
||||||
|
|
||||||
const chartsResult: ITransactionChartData = {
|
return {
|
||||||
noHits,
|
noHits,
|
||||||
tpmSeries,
|
tpmSeries,
|
||||||
responseTimeSeries
|
responseTimeSeries,
|
||||||
|
hasMLJob: timeseriesResponse.anomalyTimeseries !== undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
return chartsResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMemoryChartData extends MetricsChartAPIResponse {
|
export type MemoryMetricSeries = ReturnType<typeof getMemorySeries>;
|
||||||
series: TimeSerie[] | IEmptySeries[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMemorySeries(
|
export function getMemorySeries(
|
||||||
urlParams: IUrlParams,
|
{ start, end }: IUrlParams,
|
||||||
memoryChartResponse: MetricsChartAPIResponse['memory']
|
memoryChartResponse: MetricsChartAPIResponse['memory']
|
||||||
) {
|
) {
|
||||||
const { start, end } = urlParams;
|
|
||||||
const { series, overallValues, totalHits } = memoryChartResponse;
|
const { series, overallValues, totalHits } = memoryChartResponse;
|
||||||
const seriesList: IMemoryChartData['series'] =
|
const seriesList =
|
||||||
totalHits === 0
|
totalHits === 0
|
||||||
? getEmptySerie(start, end)
|
? getEmptySerie(start, end)
|
||||||
: [
|
: [
|
||||||
|
@ -132,14 +142,12 @@ export function getMemorySeries(
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...memoryChartResponse,
|
totalHits: memoryChartResponse.totalHits,
|
||||||
series: seriesList
|
series: seriesList
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICPUChartData extends MetricsChartAPIResponse {
|
export type CPUMetricSeries = ReturnType<typeof getCPUSeries>;
|
||||||
series: TimeSerie[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCPUSeries(CPUChartResponse: MetricsChartAPIResponse['cpu']) {
|
export function getCPUSeries(CPUChartResponse: MetricsChartAPIResponse['cpu']) {
|
||||||
const { series, overallValues } = CPUChartResponse;
|
const { series, overallValues } = CPUChartResponse;
|
||||||
|
@ -183,7 +191,7 @@ export function getCPUSeries(CPUChartResponse: MetricsChartAPIResponse['cpu']) {
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return { ...CPUChartResponse, series: seriesList };
|
return { totalHits: CPUChartResponse.totalHits, series: seriesList };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimeSerie {
|
interface TimeSerie {
|
||||||
|
|
|
@ -1,17 +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 { createSelector } from 'reselect';
|
|
||||||
import { getLicense } from 'x-pack/plugins/apm/public/store/reactReduxRequest/license';
|
|
||||||
|
|
||||||
export const selectIsMLAvailable = createSelector(
|
|
||||||
[getLicense],
|
|
||||||
license =>
|
|
||||||
license.data &&
|
|
||||||
license.data.features &&
|
|
||||||
license.data.features.ml &&
|
|
||||||
license.data.features.ml.is_available
|
|
||||||
);
|
|
|
@ -7,14 +7,11 @@
|
||||||
import datemath from '@elastic/datemath';
|
import datemath from '@elastic/datemath';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import { compact, pick } from 'lodash';
|
import { compact, pick } from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import {
|
import {
|
||||||
legacyDecodeURIComponent,
|
legacyDecodeURIComponent,
|
||||||
toQuery
|
toQuery
|
||||||
} from '../components/shared/Links/url_helpers';
|
} from '../components/shared/Links/url_helpers';
|
||||||
import { LOCATION_UPDATE } from './location';
|
import { LOCATION_UPDATE } from './location';
|
||||||
import { getDefaultTransactionType } from './reactReduxRequest/serviceDetails';
|
|
||||||
import { getDefaultDistributionSample } from './reactReduxRequest/transactionDistribution';
|
|
||||||
import { IReduxState } from './rootReducer';
|
import { IReduxState } from './rootReducer';
|
||||||
|
|
||||||
// ACTION TYPES
|
// ACTION TYPES
|
||||||
|
@ -153,16 +150,11 @@ export function toNumber(value?: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toString(str?: string | string[]) {
|
function toString(value?: string) {
|
||||||
if (
|
if (value === '' || value === 'null' || value === 'undefined') {
|
||||||
str === '' ||
|
|
||||||
str === 'null' ||
|
|
||||||
str === 'undefined' ||
|
|
||||||
Array.isArray(str)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return str;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toBoolean(value?: string) {
|
export function toBoolean(value?: string) {
|
||||||
|
@ -211,23 +203,9 @@ export function refreshTimeRange(time: TimeRange): TimeRangeRefreshAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
export const getUrlParams = createSelector(
|
export function getUrlParams(state: IReduxState) {
|
||||||
(state: IReduxState) => state.urlParams,
|
return state.urlParams;
|
||||||
getDefaultTransactionType,
|
|
||||||
getDefaultDistributionSample,
|
|
||||||
(
|
|
||||||
urlParams,
|
|
||||||
transactionType: string,
|
|
||||||
{ traceId, transactionId }
|
|
||||||
): IUrlParams => {
|
|
||||||
return {
|
|
||||||
transactionType,
|
|
||||||
transactionId,
|
|
||||||
traceId,
|
|
||||||
...urlParams
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
export interface IUrlParams {
|
export interface IUrlParams {
|
||||||
detailTab?: string;
|
detailTab?: string;
|
||||||
|
|
|
@ -8,11 +8,9 @@
|
||||||
|
|
||||||
import { mount, ReactWrapper } from 'enzyme';
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
import enzymeToJson from 'enzyme-to-json';
|
import enzymeToJson from 'enzyme-to-json';
|
||||||
import createHistory from 'history/createHashHistory';
|
|
||||||
import 'jest-styled-components';
|
import 'jest-styled-components';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Moment } from 'moment-timezone';
|
import { Moment } from 'moment-timezone';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
@ -29,51 +27,6 @@ export function toJson(wrapper: ReactWrapper) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultRoute = {
|
|
||||||
match: { path: '/', url: '/', params: {}, isExact: true },
|
|
||||||
location: { pathname: '/', search: '', hash: '', key: '4yyjf5' }
|
|
||||||
};
|
|
||||||
|
|
||||||
export function mountWithRouterAndStore(
|
|
||||||
Component: React.ReactElement,
|
|
||||||
storeState = {},
|
|
||||||
route = defaultRoute
|
|
||||||
) {
|
|
||||||
const store = createMockStore(storeState);
|
|
||||||
const history = createHistory();
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
context: {
|
|
||||||
store,
|
|
||||||
router: {
|
|
||||||
history,
|
|
||||||
route
|
|
||||||
}
|
|
||||||
},
|
|
||||||
childContextTypes: {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
router: PropTypes.object.isRequired
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return mount(Component, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mountWithStore(Component: React.ReactElement, storeState = {}) {
|
|
||||||
const store = createMockStore(storeState);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
context: {
|
|
||||||
store
|
|
||||||
},
|
|
||||||
childContextTypes: {
|
|
||||||
store: PropTypes.object.isRequired
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return mount(Component, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mockMoment() {
|
export function mockMoment() {
|
||||||
// avoid timezone issues
|
// avoid timezone issues
|
||||||
jest
|
jest
|
||||||
|
@ -90,11 +43,6 @@ export function mockMoment() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Await this when you need to "flush" promises to immediately resolve or throw in tests
|
|
||||||
export async function asyncFlush() {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Useful for getting the rendered href from any kind of link component
|
// Useful for getting the rendered href from any kind of link component
|
||||||
export async function getRenderedHref(
|
export async function getRenderedHref(
|
||||||
Component: React.FunctionComponent<{}>,
|
Component: React.FunctionComponent<{}>,
|
||||||
|
@ -109,7 +57,7 @@ export async function getRenderedHref(
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
await asyncFlush();
|
await tick();
|
||||||
|
|
||||||
return mounted.render().attr('href');
|
return mounted.render().attr('href');
|
||||||
}
|
}
|
||||||
|
@ -118,3 +66,10 @@ export function mockNow(date: string) {
|
||||||
const fakeNow = new Date(date).getTime();
|
const fakeNow = new Date(date).getTime();
|
||||||
return jest.spyOn(Date, 'now').mockReturnValue(fakeNow);
|
return jest.spyOn(Date, 'now').mockReturnValue(fakeNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function delay(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Await this when you need to "flush" promises to immediately resolve or throw in tests
|
||||||
|
export const tick = () => new Promise(resolve => setImmediate(resolve, 0));
|
||||||
|
|
|
@ -1,36 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Everything in here should be moved to http://github.com/sqren/react-redux-request
|
|
||||||
|
|
||||||
declare module 'react-redux-request' {
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// status and args are optional, especially for places that use initial data in a reducer
|
|
||||||
export interface RRRRenderResponse<T, P = any[]> {
|
|
||||||
status?: 'SUCCESS' | 'LOADING' | 'FAILURE';
|
|
||||||
data: T;
|
|
||||||
args?: P;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RRRRender<T, P = any[]> = (
|
|
||||||
res: RRRRenderResponse<T, P>
|
|
||||||
) => React.ReactNode;
|
|
||||||
|
|
||||||
export interface RequestProps<T, P> {
|
|
||||||
id: string;
|
|
||||||
fn: (args: any) => Promise<any>;
|
|
||||||
selector?: (state: any) => any;
|
|
||||||
args?: any[];
|
|
||||||
render?: RRRRender<T, P>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reducer(state: any): any;
|
|
||||||
|
|
||||||
export class Request<T, P = any[]> extends React.Component<
|
|
||||||
RequestProps<T, P>
|
|
||||||
> {}
|
|
||||||
}
|
|
116
yarn.lock
116
yarn.lock
|
@ -772,13 +772,20 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.12.0"
|
regenerator-runtime "^0.12.0"
|
||||||
|
|
||||||
"@babel/runtime@^7.3.4":
|
"@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4":
|
||||||
version "7.3.4"
|
version "7.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
|
||||||
integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
|
integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.12.0"
|
regenerator-runtime "^0.12.0"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.4.2":
|
||||||
|
version "7.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.2.tgz#f5ab6897320f16decd855eed70b705908a313fe8"
|
||||||
|
integrity sha512-7Bl2rALb7HpvXFL7TETNzKSAeBVCPHELzc0C//9FCxN8nsiueWSJBqaF+2oIJScyILStASR/Cx5WMkXGYTiJFA==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2":
|
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2":
|
||||||
version "7.2.2"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
|
||||||
|
@ -1057,6 +1064,14 @@
|
||||||
normalize-path "^2.0.1"
|
normalize-path "^2.0.1"
|
||||||
through2 "^2.0.3"
|
through2 "^2.0.3"
|
||||||
|
|
||||||
|
"@jest/types@^24.5.0":
|
||||||
|
version "24.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.5.0.tgz#feee214a4d0167b0ca447284e95a57aa10b3ee95"
|
||||||
|
integrity sha512-kN7RFzNMf2R8UDadPOl6ReyI+MT8xfqRuAnuVL+i4gwjv/zubdDK+EDeLHYwq1j0CSSR2W/MmgaRlMZJzXdmVA==
|
||||||
|
dependencies:
|
||||||
|
"@types/istanbul-lib-coverage" "^1.1.0"
|
||||||
|
"@types/yargs" "^12.0.9"
|
||||||
|
|
||||||
"@mapbox/geojson-area@0.2.2":
|
"@mapbox/geojson-area@0.2.2":
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz#18d7814aa36bf23fbbcc379f8e26a22927debf10"
|
resolved "https://registry.yarnpkg.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz#18d7814aa36bf23fbbcc379f8e26a22927debf10"
|
||||||
|
@ -1143,6 +1158,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
url-pattern "^1.0.3"
|
url-pattern "^1.0.3"
|
||||||
|
|
||||||
|
"@sheerun/mutationobserver-shim@^0.3.2":
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
|
||||||
|
integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==
|
||||||
|
|
||||||
"@sindresorhus/is@^0.7.0":
|
"@sindresorhus/is@^0.7.0":
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
||||||
|
@ -1946,6 +1966,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/istanbul-lib-coverage@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.0.tgz#2cc2ca41051498382b43157c8227fea60363f94a"
|
||||||
|
integrity sha512-ohkhb9LehJy+PA40rDtGAji61NCgdtKLAlFoYp4cnuuQEswwdK3vz9SOIkkyc3wrk8dzjphQApNs56yyXLStaQ==
|
||||||
|
|
||||||
"@types/jest-diff@*":
|
"@types/jest-diff@*":
|
||||||
version "20.0.1"
|
version "20.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89"
|
resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89"
|
||||||
|
@ -2046,6 +2071,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8"
|
resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8"
|
||||||
integrity sha512-TzzIZihV+y9kxSg5xJMkyIkaoGkXi50isZTtGHObNHRqAAwjGNjSCNPI7AUAv0tZUKTq9f2cdkCUd/2JVZUTrA==
|
integrity sha512-TzzIZihV+y9kxSg5xJMkyIkaoGkXi50isZTtGHObNHRqAAwjGNjSCNPI7AUAv0tZUKTq9f2cdkCUd/2JVZUTrA==
|
||||||
|
|
||||||
|
"@types/lru-cache@^5.1.0":
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
|
||||||
|
integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
|
||||||
|
|
||||||
"@types/mime-db@*":
|
"@types/mime-db@*":
|
||||||
version "1.27.0"
|
version "1.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime-db/-/mime-db-1.27.0.tgz#9bc014a1fd1fdf47649c1a54c6dd7966b8284792"
|
resolved "https://registry.yarnpkg.com/@types/mime-db/-/mime-db-1.27.0.tgz#9bc014a1fd1fdf47649c1a54c6dd7966b8284792"
|
||||||
|
@ -2114,6 +2144,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271"
|
resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271"
|
||||||
integrity sha512-ShHzHkYD+Ldw3eyttptCpUhF1/mkInWwasQkCNXZHOsJMJ/UMa8wXrxSrTJaVk0r4pLK/VnESVM0wFsfQzNEKQ==
|
integrity sha512-ShHzHkYD+Ldw3eyttptCpUhF1/mkInWwasQkCNXZHOsJMJ/UMa8wXrxSrTJaVk0r4pLK/VnESVM0wFsfQzNEKQ==
|
||||||
|
|
||||||
|
"@types/object-hash@^1.2.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.2.0.tgz#d65904331bd0b05c7d5ece75f9ddfdbe82affd30"
|
||||||
|
integrity sha512-0JKYQRatHdzijO/ni7JV5eHUJWaMRpGvwiABk8U5iAk5Corm0yLNEfYGNkZWYc+wCyCKKpg0+TsZIvP8AymIYA==
|
||||||
|
|
||||||
"@types/opn@^5.1.0":
|
"@types/opn@^5.1.0":
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/opn/-/opn-5.1.0.tgz#bff7bc371677f4bdbb37884400e03fd81f743927"
|
resolved "https://registry.yarnpkg.com/@types/opn/-/opn-5.1.0.tgz#bff7bc371677f4bdbb37884400e03fd81f743927"
|
||||||
|
@ -2421,6 +2456,11 @@
|
||||||
"@types/events" "*"
|
"@types/events" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/yargs@^12.0.9":
|
||||||
|
version "12.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.10.tgz#17a8ec65cd8e88f51b418ceb271af18d3137df67"
|
||||||
|
integrity sha512-WsVzTPshvCSbHThUduGGxbmnwcpkgSctHGHTqzWyFg4lYAuV5qXlyFPOsP3OWqCINfmg/8VXP+zJaa4OxEsBQQ==
|
||||||
|
|
||||||
"@types/zen-observable@^0.8.0":
|
"@types/zen-observable@^0.8.0":
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
|
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
|
||||||
|
@ -7686,6 +7726,26 @@ dom-serializer@0, dom-serializer@~0.1.0:
|
||||||
domelementtype "^1.3.0"
|
domelementtype "^1.3.0"
|
||||||
entities "^1.1.1"
|
entities "^1.1.1"
|
||||||
|
|
||||||
|
dom-testing-library@^3.13.1:
|
||||||
|
version "3.17.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.17.1.tgz#3291bc3cf68c555ba5e663697ee77d604aaa122b"
|
||||||
|
integrity sha512-SbkaRfQvuLjnv+xFgSo/cmKoN9tjBL6Rh1f3nQH9jnjUe5q+keRwacYSi3uSpcB4D1K768iavCayKH3ZN9ea+g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.3.4"
|
||||||
|
"@sheerun/mutationobserver-shim" "^0.3.2"
|
||||||
|
pretty-format "^24.0.0"
|
||||||
|
wait-for-expect "^1.1.0"
|
||||||
|
|
||||||
|
dom-testing-library@^3.18.2:
|
||||||
|
version "3.18.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.18.2.tgz#07d65166743ad3299b7bee5b488e9622c31241bc"
|
||||||
|
integrity sha512-+nYUgGhHarrCY8kLVmyHlgM+IGwBXXrYsWIJB6vpAx2ne9WFgKfwMGcOkkTKQhuAro0sP6RIuRGfm5NF3+ccmQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.3.4"
|
||||||
|
"@sheerun/mutationobserver-shim" "^0.3.2"
|
||||||
|
pretty-format "^24.5.0"
|
||||||
|
wait-for-expect "^1.1.0"
|
||||||
|
|
||||||
dom-walk@^0.1.0:
|
dom-walk@^0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
|
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
|
||||||
|
@ -16389,6 +16449,11 @@ object-copy@^0.1.0:
|
||||||
define-property "^0.2.5"
|
define-property "^0.2.5"
|
||||||
kind-of "^3.0.3"
|
kind-of "^3.0.3"
|
||||||
|
|
||||||
|
object-hash@^1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
|
||||||
|
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
|
||||||
|
|
||||||
object-inspect@^1.6.0:
|
object-inspect@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
|
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
|
||||||
|
@ -17672,6 +17737,16 @@ pretty-format@^24.0.0:
|
||||||
ansi-regex "^4.0.0"
|
ansi-regex "^4.0.0"
|
||||||
ansi-styles "^3.2.0"
|
ansi-styles "^3.2.0"
|
||||||
|
|
||||||
|
pretty-format@^24.5.0:
|
||||||
|
version "24.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.5.0.tgz#cc69a0281a62cd7242633fc135d6930cd889822d"
|
||||||
|
integrity sha512-/3RuSghukCf8Riu5Ncve0iI+BzVkbRU5EeUoArKARZobREycuH5O4waxvaNIloEXdb0qwgmEAed5vTpX1HNROQ==
|
||||||
|
dependencies:
|
||||||
|
"@jest/types" "^24.5.0"
|
||||||
|
ansi-regex "^4.0.0"
|
||||||
|
ansi-styles "^3.2.0"
|
||||||
|
react-is "^16.8.4"
|
||||||
|
|
||||||
pretty-hrtime@^1.0.0:
|
pretty-hrtime@^1.0.0:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
||||||
|
@ -18483,6 +18558,14 @@ react-grid-layout@^0.16.2:
|
||||||
react-draggable "3.x"
|
react-draggable "3.x"
|
||||||
react-resizable "1.x"
|
react-resizable "1.x"
|
||||||
|
|
||||||
|
react-hooks-testing-library@^0.3.8:
|
||||||
|
version "0.3.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-hooks-testing-library/-/react-hooks-testing-library-0.3.8.tgz#717595ed7be500023963dd502f188aa932bf70f0"
|
||||||
|
integrity sha512-YFnyd2jH2voikSBGufqhprnxMTHgosOHlO5EXhuQycWxfeTCIiw/17aiYbpvRRDRB/0j8QvI/jHXMNVBKw7WzA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.4.2"
|
||||||
|
react-testing-library "^6.0.2"
|
||||||
|
|
||||||
react-input-autosize@^2.1.2, react-input-autosize@^2.2.1:
|
react-input-autosize@^2.1.2, react-input-autosize@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
|
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
|
||||||
|
@ -18524,6 +18607,11 @@ react-is@^16.8.1, react-is@^16.8.2:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.2.tgz#09891d324cad1cb0c1f2d91f70a71a4bee34df0f"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.2.tgz#09891d324cad1cb0c1f2d91f70a71a4bee34df0f"
|
||||||
integrity sha512-D+NxhSR2HUCjYky1q1DwpNUD44cDpUXzSmmFyC3ug1bClcU/iDNy0YNn1iwme28fn+NFhpA13IndOd42CrFb+Q==
|
integrity sha512-D+NxhSR2HUCjYky1q1DwpNUD44cDpUXzSmmFyC3ug1bClcU/iDNy0YNn1iwme28fn+NFhpA13IndOd42CrFb+Q==
|
||||||
|
|
||||||
|
react-is@^16.8.4:
|
||||||
|
version "16.8.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.5.tgz#c54ac229dd66b5afe0de5acbe47647c3da692ff8"
|
||||||
|
integrity sha512-sudt2uq5P/2TznPV4Wtdi+Lnq3yaYW8LfvPKLM9BKD8jJNBkxMVyB0C9/GmVhLw7Jbdmndk/73n7XQGeN9A3QQ==
|
||||||
|
|
||||||
react-is@~16.3.0:
|
react-is@~16.3.0:
|
||||||
version "16.3.2"
|
version "16.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22"
|
||||||
|
@ -18770,6 +18858,22 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.0:
|
||||||
react-is "^16.8.2"
|
react-is "^16.8.2"
|
||||||
scheduler "^0.13.2"
|
scheduler "^0.13.2"
|
||||||
|
|
||||||
|
react-testing-library@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.0.tgz#81edfcfae8a795525f48685be9bf561df45bb35d"
|
||||||
|
integrity sha512-h0h+YLe4KWptK6HxOMnoNN4ngu3W8isrwDmHjPC5gxc+nOZOCurOvbKVYCvvuAw91jdO7VZSm/5KR7TxKnz0qA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.3.1"
|
||||||
|
dom-testing-library "^3.13.1"
|
||||||
|
|
||||||
|
react-testing-library@^6.0.2:
|
||||||
|
version "6.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.3.tgz#8b5d276a353c17ce4f7486015bb7a1c8827c442c"
|
||||||
|
integrity sha512-tN0A6nywSOoL8kriqru3rSdw31PxuquL7xnW6xBI0aTNw0VO3kZQtaEa0npUH9dX0MIsSunB0nbElRrc4VtAzw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.4.2"
|
||||||
|
dom-testing-library "^3.18.2"
|
||||||
|
|
||||||
react-textarea-autosize@^7.0.4:
|
react-textarea-autosize@^7.0.4:
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-7.0.4.tgz#4e4be649b544a88713e7b5043f76950f35d3d503"
|
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-7.0.4.tgz#4e4be649b544a88713e7b5043f76950f35d3d503"
|
||||||
|
@ -19238,6 +19342,11 @@ regenerator-runtime@^0.12.0, regenerator-runtime@^0.12.1:
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
||||||
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
||||||
|
|
||||||
|
regenerator-runtime@^0.13.2:
|
||||||
|
version "0.13.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
|
||||||
|
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
|
||||||
|
|
||||||
regenerator-transform@^0.13.3:
|
regenerator-transform@^0.13.3:
|
||||||
version "0.13.3"
|
version "0.13.3"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb"
|
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb"
|
||||||
|
@ -23911,6 +24020,11 @@ w3c-hr-time@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
browser-process-hrtime "^0.1.2"
|
browser-process-hrtime "^0.1.2"
|
||||||
|
|
||||||
|
wait-for-expect@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453"
|
||||||
|
integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==
|
||||||
|
|
||||||
walk@2.3.x:
|
walk@2.3.x:
|
||||||
version "2.3.9"
|
version "2.3.9"
|
||||||
resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.9.tgz#31b4db6678f2ae01c39ea9fb8725a9031e558a7b"
|
resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.9.tgz#31b4db6678f2ae01c39ea9fb8725a9031e558a7b"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue