Use platform history (#74328)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: cauemarcondes <caue.marcondes@elastic.co>
This commit is contained in:
Nathan L Smith 2020-08-31 08:43:35 -05:00 committed by GitHub
parent 833d76442f
commit 9b5912203f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 1215 additions and 771 deletions

View file

@ -0,0 +1,74 @@
/*
* 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 { act } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Observable } from 'rxjs';
import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public';
import { mockApmPluginContextValue } from '../context/ApmPluginContext/MockApmPluginContext';
import { ApmPluginSetupDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/createCallApmApi';
import { renderApp } from './';
import { disableConsoleWarning } from '../utils/testHelpers';
describe('renderApp', () => {
let mockConsole: jest.SpyInstance;
beforeAll(() => {
// The RUM agent logs an unnecessary message here. There's a couple open
// issues need to be fixed to get the ability to turn off all of the logging:
//
// * https://github.com/elastic/apm-agent-rum-js/issues/799
// * https://github.com/elastic/apm-agent-rum-js/issues/861
//
// for now, override `console.warn` to filter those messages out.
mockConsole = disableConsoleWarning('[Elastic APM]');
});
afterAll(() => {
mockConsole.mockRestore();
});
it('renders the app', () => {
const { core, config } = mockApmPluginContextValue;
const plugins = {
licensing: { license$: new Observable() },
triggers_actions_ui: { actionTypeRegistry: {}, alertTypeRegistry: {} },
usageCollection: { reportUiStats: () => {} },
};
const params = {
element: document.createElement('div'),
history: createMemoryHistory(),
};
jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined);
createCallApmApi((core.http as unknown) as HttpSetup);
jest
.spyOn(window.console, 'warn')
.mockImplementationOnce((message: string) => {
if (message.startsWith('[Elastic APM')) {
return;
} else {
console.warn(message); // eslint-disable-line no-console
}
});
let unmount: () => void;
act(() => {
unmount = renderApp(
(core as unknown) as CoreStart,
(plugins as unknown) as ApmPluginSetupDeps,
(params as unknown) as AppMountParameters,
config
);
});
expect(() => {
unmount();
}).not.toThrowError();
});
});

View file

@ -16,11 +16,11 @@ import { ApmPluginSetupDeps } from '../plugin';
import {
KibanaContextProvider,
useUiSetting$,
RedirectAppLinks,
} from '../../../../../src/plugins/kibana_react/public';
import { px, units } from '../style/variables';
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { history, resetHistory } from '../utils/history';
import 'react-vis/dist/style.css';
import { RumHome } from '../components/app/RumDashboard/RumHome';
import { ConfigSchema } from '../index';
@ -70,12 +70,12 @@ function CsmApp() {
export function CsmAppRoot({
core,
deps,
routerHistory,
history,
config,
}: {
core: CoreStart;
deps: ApmPluginSetupDeps;
routerHistory: typeof history;
history: AppMountParameters['history'];
config: ConfigSchema;
}) {
const i18nCore = core.i18n;
@ -86,19 +86,21 @@ export function CsmAppRoot({
plugins,
};
return (
<ApmPluginContext.Provider value={apmPluginContextValue}>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<i18nCore.Context>
<Router history={routerHistory}>
<UrlParamsProvider>
<LoadingIndicatorProvider>
<CsmApp />
</LoadingIndicatorProvider>
</UrlParamsProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</ApmPluginContext.Provider>
<RedirectAppLinks application={core.application}>
<ApmPluginContext.Provider value={apmPluginContextValue}>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<i18nCore.Context>
<Router history={history}>
<UrlParamsProvider>
<LoadingIndicatorProvider>
<CsmApp />
</LoadingIndicatorProvider>
</UrlParamsProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</ApmPluginContext.Provider>
</RedirectAppLinks>
);
}
@ -109,19 +111,13 @@ export function CsmAppRoot({
export const renderApp = (
core: CoreStart,
deps: ApmPluginSetupDeps,
{ element }: AppMountParameters,
{ element, history }: AppMountParameters,
config: ConfigSchema
) => {
createCallApmApi(core.http);
resetHistory();
ReactDOM.render(
<CsmAppRoot
core={core}
deps={deps}
routerHistory={history}
config={config}
/>,
<CsmAppRoot core={core} deps={deps} history={history} config={config} />,
element
);
return () => {

View file

@ -5,36 +5,36 @@
*/
import { ApmRoute } from '@elastic/apm-rum-react';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
import styled, { ThemeProvider, DefaultTheme } from 'styled-components';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { CoreStart, AppMountParameters } from '../../../../../src/core/public';
import { ApmPluginSetupDeps } from '../plugin';
import 'react-vis/dist/style.css';
import styled, { DefaultTheme, ThemeProvider } from 'styled-components';
import { ConfigSchema } from '../';
import { AppMountParameters, CoreStart } from '../../../../../src/core/public';
import {
KibanaContextProvider,
RedirectAppLinks,
useUiSetting$,
} from '../../../../../src/plugins/kibana_react/public';
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
import { routes } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { ApmPluginContext } from '../context/ApmPluginContext';
import { LicenseProvider } from '../context/LicenseContext';
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
import { LocationProvider } from '../context/LocationContext';
import { MatchedRouteProvider } from '../context/MatchedRouteContext';
import { UrlParamsProvider } from '../context/UrlParamsContext';
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import {
KibanaContextProvider,
useUiSetting$,
} from '../../../../../src/plugins/kibana_react/public';
import { px, units } from '../style/variables';
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { routes } from '../components/app/Main/route_config';
import { history, resetHistory } from '../utils/history';
import { setHelpExtension } from '../setHelpExtension';
import { setReadonlyBadge } from '../updateBadge';
import { ApmPluginSetupDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/createCallApmApi';
import { ConfigSchema } from '..';
import 'react-vis/dist/style.css';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import { setHelpExtension } from '../setHelpExtension';
import { px, units } from '../style/variables';
import { setReadonlyBadge } from '../updateBadge';
const MainContainer = styled.div`
padding: ${px(units.plus)};
@ -68,12 +68,12 @@ function App() {
export function ApmAppRoot({
core,
deps,
routerHistory,
history,
config,
}: {
core: CoreStart;
deps: ApmPluginSetupDeps;
routerHistory: typeof history;
history: AppMountParameters['history'];
config: ConfigSchema;
}) {
const i18nCore = core.i18n;
@ -84,36 +84,38 @@ export function ApmAppRoot({
plugins,
};
return (
<ApmPluginContext.Provider value={apmPluginContextValue}>
<AlertsContextProvider
value={{
http: core.http,
docLinks: core.docLinks,
capabilities: core.application.capabilities,
toastNotifications: core.notifications.toasts,
actionTypeRegistry: plugins.triggers_actions_ui.actionTypeRegistry,
alertTypeRegistry: plugins.triggers_actions_ui.alertTypeRegistry,
}}
>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<i18nCore.Context>
<Router history={routerHistory}>
<LocationProvider>
<MatchedRouteProvider routes={routes}>
<UrlParamsProvider>
<LoadingIndicatorProvider>
<LicenseProvider>
<App />
</LicenseProvider>
</LoadingIndicatorProvider>
</UrlParamsProvider>
</MatchedRouteProvider>
</LocationProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</AlertsContextProvider>
</ApmPluginContext.Provider>
<RedirectAppLinks application={core.application}>
<ApmPluginContext.Provider value={apmPluginContextValue}>
<AlertsContextProvider
value={{
http: core.http,
docLinks: core.docLinks,
capabilities: core.application.capabilities,
toastNotifications: core.notifications.toasts,
actionTypeRegistry: plugins.triggers_actions_ui.actionTypeRegistry,
alertTypeRegistry: plugins.triggers_actions_ui.alertTypeRegistry,
}}
>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<i18nCore.Context>
<Router history={history}>
<LocationProvider>
<MatchedRouteProvider routes={routes}>
<UrlParamsProvider>
<LoadingIndicatorProvider>
<LicenseProvider>
<App />
</LicenseProvider>
</LoadingIndicatorProvider>
</UrlParamsProvider>
</MatchedRouteProvider>
</LocationProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</AlertsContextProvider>
</ApmPluginContext.Provider>
</RedirectAppLinks>
);
}
@ -124,7 +126,7 @@ export function ApmAppRoot({
export const renderApp = (
core: CoreStart,
deps: ApmPluginSetupDeps,
{ element }: AppMountParameters,
{ element, history }: AppMountParameters,
config: ConfigSchema
) => {
// render APM feedback link in global help menu
@ -133,8 +135,6 @@ export const renderApp = (
createCallApmApi(core.http);
resetHistory();
// Automatically creates static index pattern and stores as saved object
createStaticIndexPattern().catch((e) => {
// eslint-disable-next-line no-console
@ -142,12 +142,7 @@ export const renderApp = (
});
ReactDOM.render(
<ApmAppRoot
core={core}
deps={deps}
routerHistory={history}
config={config}
/>,
<ApmAppRoot core={core} deps={deps} history={history} config={config} />,
element
);
return () => {

View file

@ -6,40 +6,40 @@
import {
EuiButtonEmpty,
EuiIcon,
EuiPanel,
EuiSpacer,
EuiTab,
EuiTabs,
EuiTitle,
EuiIcon,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React from 'react';
import styled from 'styled-components';
import { first } from 'lodash';
import React from 'react';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { px, unit, units } from '../../../../style/variables';
import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata';
import { Stacktrace } from '../../../shared/Stacktrace';
import { Summary } from '../../../shared/Summary';
import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem';
import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem';
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
import {
ErrorTab,
exceptionStacktraceTab,
getTabs,
logStacktraceTab,
} from './ErrorTabs';
import { Summary } from '../../../shared/Summary';
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem';
import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem';
import { ExceptionStacktrace } from './ExceptionStacktrace';
const HeaderContainer = styled.div`
@ -71,6 +71,7 @@ function getCurrentTab(
}
export function DetailView({ errorGroup, urlParams, location }: Props) {
const history = useHistory();
const { transaction, error, occurrencesCount } = errorGroup;
if (!error) {

View file

@ -6,10 +6,11 @@
import { mount } from 'enzyme';
import React from 'react';
import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext';
import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider';
import { mockMoment, toJson } from '../../../../../utils/testHelpers';
import { ErrorGroupList } from '../index';
import props from './props.json';
import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
return {
@ -36,9 +37,11 @@ describe('ErrorGroupOverview -> List', () => {
it('should render with data', () => {
const wrapper = mount(
<MockUrlParamsContextProvider>
<ErrorGroupList items={props.items} />
</MockUrlParamsContextProvider>
<MockApmPluginContextWrapper>
<MockUrlParamsContextProvider>
<ErrorGroupList items={props.items} />
</MockUrlParamsContextProvider>
</MockApmPluginContextWrapper>
);
expect(toJson(wrapper)).toMatchSnapshot();

View file

@ -784,11 +784,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c0"
href="#/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee?"
href="/basepath/app/apm/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee"
>
<a
className="euiLink euiLink--primary c0"
href="#/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee?"
href="/basepath/app/apm/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee"
rel="noreferrer"
>
a0ce2
@ -829,11 +829,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
>
<a
className="euiLink euiLink--primary c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
rel="noreferrer"
/>
</EuiLink>
@ -876,13 +876,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c3"
href="#/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee?"
href="/basepath/app/apm/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee"
onBlur={[Function]}
onFocus={[Function]}
>
<a
className="euiLink euiLink--primary c3"
href="#/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee?"
href="/basepath/app/apm/services/opbeans-python/errors/a0ce2c8978ef92cdf2ff163ae28576ee"
onBlur={[Function]}
onFocus={[Function]}
rel="noreferrer"
@ -1018,11 +1018,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c0"
href="#/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a?"
href="/basepath/app/apm/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a"
>
<a
className="euiLink euiLink--primary c0"
href="#/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a?"
href="/basepath/app/apm/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a"
rel="noreferrer"
>
f3ac9
@ -1063,11 +1063,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
>
<a
className="euiLink euiLink--primary c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
rel="noreferrer"
/>
</EuiLink>
@ -1110,13 +1110,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c3"
href="#/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a?"
href="/basepath/app/apm/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a"
onBlur={[Function]}
onFocus={[Function]}
>
<a
className="euiLink euiLink--primary c3"
href="#/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a?"
href="/basepath/app/apm/services/opbeans-python/errors/f3ac95493913cc7a3cfec30a19d2120a"
onBlur={[Function]}
onFocus={[Function]}
rel="noreferrer"
@ -1252,11 +1252,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c0"
href="#/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840?"
href="/basepath/app/apm/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840"
>
<a
className="euiLink euiLink--primary c0"
href="#/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840?"
href="/basepath/app/apm/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840"
rel="noreferrer"
>
e9086
@ -1297,11 +1297,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
>
<a
className="euiLink euiLink--primary c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
rel="noreferrer"
/>
</EuiLink>
@ -1344,13 +1344,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c3"
href="#/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840?"
href="/basepath/app/apm/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840"
onBlur={[Function]}
onFocus={[Function]}
>
<a
className="euiLink euiLink--primary c3"
href="#/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840?"
href="/basepath/app/apm/services/opbeans-python/errors/e90863d04b7a692435305f09bbe8c840"
onBlur={[Function]}
onFocus={[Function]}
rel="noreferrer"
@ -1486,11 +1486,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c0"
href="#/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc?"
href="/basepath/app/apm/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc"
>
<a
className="euiLink euiLink--primary c0"
href="#/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc?"
href="/basepath/app/apm/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc"
rel="noreferrer"
>
8673d
@ -1531,11 +1531,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
>
<a
className="euiLink euiLink--primary c1"
href="#/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
href="/basepath/app/apm/services/opbeans-python/errors?page=0&serviceName=opbeans-python&transactionType=request&start=2018-01-10T09:51:41.050Z&end=2018-01-10T10:06:41.050Z&kuery=error.exception.type:undefined"
rel="noreferrer"
/>
</EuiLink>
@ -1578,13 +1578,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
<EuiLink
className="c3"
href="#/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc?"
href="/basepath/app/apm/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc"
onBlur={[Function]}
onFocus={[Function]}
>
<a
className="euiLink euiLink--primary c3"
href="#/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc?"
href="/basepath/app/apm/services/opbeans-python/errors/8673d8bf7a032e387c101bafbab0d2bc"
onBlur={[Function]}
onFocus={[Function]}
rel="noreferrer"

View file

@ -11,8 +11,21 @@ exports[`Home component should render services 1`] = `
},
},
"core": Object {
"application": Object {
"capabilities": Object {
"apm": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
},
},
"chrome": Object {
"docTitle": Object {
"change": [Function],
},
"setBadge": [Function],
"setBreadcrumbs": [Function],
"setHelpExtension": [Function],
},
"docLinks": Object {
"DOC_LINK_VERSION": "0",
@ -23,6 +36,9 @@ exports[`Home component should render services 1`] = `
"prepend": [Function],
},
},
"i18n": Object {
"Context": [Function],
},
"notifications": Object {
"toasts": Object {
"addDanger": [Function],
@ -31,6 +47,7 @@ exports[`Home component should render services 1`] = `
},
"uiSettings": Object {
"get": [Function],
"get$": [Function],
},
},
"plugins": Object {},
@ -54,8 +71,21 @@ exports[`Home component should render traces 1`] = `
},
},
"core": Object {
"application": Object {
"capabilities": Object {
"apm": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
},
},
"chrome": Object {
"docTitle": Object {
"change": [Function],
},
"setBadge": [Function],
"setBreadcrumbs": [Function],
"setHelpExtension": [Function],
},
"docLinks": Object {
"DOC_LINK_VERSION": "0",
@ -66,6 +96,9 @@ exports[`Home component should render traces 1`] = `
"prepend": [Function],
},
},
"i18n": Object {
"Context": [Function],
},
"notifications": Object {
"toasts": Object {
"addDanger": [Function],
@ -74,6 +107,7 @@ exports[`Home component should render traces 1`] = `
},
"uiSettings": Object {
"get": [Function],
"get$": [Function],
},
},
"plugins": Object {},

View file

@ -60,29 +60,32 @@ describe('UpdateBreadcrumbs', () => {
'rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
);
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
expect(breadcrumbs).toEqual([
{
text: 'APM',
href:
'#/?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
},
{
text: 'Services',
href:
'#/services?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
},
{
text: 'opbeans-node',
href:
'#/services/opbeans-node?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
},
{
text: 'Errors',
href:
'#/services/opbeans-node/errors?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
},
{ text: 'myGroupId', href: undefined },
]);
expect(breadcrumbs).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href:
'/basepath/app/apm/?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({
text: 'Services',
href:
'/basepath/app/apm/services?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({
text: 'opbeans-node',
href:
'/basepath/app/apm/services/opbeans-node?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({
text: 'Errors',
href:
'/basepath/app/apm/services/opbeans-node/errors?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({ text: 'myGroupId', href: undefined }),
])
);
expect(changeTitle).toHaveBeenCalledWith([
'myGroupId',
'Errors',
@ -95,12 +98,23 @@ describe('UpdateBreadcrumbs', () => {
it('/services/:serviceName/errors', () => {
mountBreadcrumb('/services/opbeans-node/errors');
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
expect(breadcrumbs).toEqual([
{ text: 'APM', href: '#/?kuery=myKuery' },
{ text: 'Services', href: '#/services?kuery=myKuery' },
{ text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' },
{ text: 'Errors', href: undefined },
]);
expect(breadcrumbs).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href: '/basepath/app/apm/?kuery=myKuery',
}),
expect.objectContaining({
text: 'Services',
href: '/basepath/app/apm/services?kuery=myKuery',
}),
expect.objectContaining({
text: 'opbeans-node',
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
}),
expect.objectContaining({ text: 'Errors', href: undefined }),
])
);
expect(changeTitle).toHaveBeenCalledWith([
'Errors',
'opbeans-node',
@ -112,12 +126,24 @@ describe('UpdateBreadcrumbs', () => {
it('/services/:serviceName/transactions', () => {
mountBreadcrumb('/services/opbeans-node/transactions');
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
expect(breadcrumbs).toEqual([
{ text: 'APM', href: '#/?kuery=myKuery' },
{ text: 'Services', href: '#/services?kuery=myKuery' },
{ text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' },
{ text: 'Transactions', href: undefined },
]);
expect(breadcrumbs).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href: '/basepath/app/apm/?kuery=myKuery',
}),
expect.objectContaining({
text: 'Services',
href: '/basepath/app/apm/services?kuery=myKuery',
}),
expect.objectContaining({
text: 'opbeans-node',
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
}),
expect.objectContaining({ text: 'Transactions', href: undefined }),
])
);
expect(changeTitle).toHaveBeenCalledWith([
'Transactions',
'opbeans-node',
@ -132,16 +158,33 @@ describe('UpdateBreadcrumbs', () => {
'transactionName=my-transaction-name'
);
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
expect(breadcrumbs).toEqual([
{ text: 'APM', href: '#/?kuery=myKuery' },
{ text: 'Services', href: '#/services?kuery=myKuery' },
{ text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' },
{
text: 'Transactions',
href: '#/services/opbeans-node/transactions?kuery=myKuery',
},
{ text: 'my-transaction-name', href: undefined },
]);
expect(breadcrumbs).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href: '/basepath/app/apm/?kuery=myKuery',
}),
expect.objectContaining({
text: 'Services',
href: '/basepath/app/apm/services?kuery=myKuery',
}),
expect.objectContaining({
text: 'opbeans-node',
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
}),
expect.objectContaining({
text: 'Transactions',
href:
'/basepath/app/apm/services/opbeans-node/transactions?kuery=myKuery',
}),
expect.objectContaining({
text: 'my-transaction-name',
href: undefined,
}),
])
);
expect(changeTitle).toHaveBeenCalledWith([
'my-transaction-name',
'Transactions',

View file

@ -5,20 +5,20 @@
*/
import { Location } from 'history';
import React from 'react';
import { AppMountContext } from 'src/core/public';
import React, { MouseEvent } from 'react';
import { CoreStart } from 'src/core/public';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { getAPMHref } from '../../shared/Links/apm/APMLink';
import {
Breadcrumb,
ProvideBreadcrumbs,
BreadcrumbRoute,
ProvideBreadcrumbs,
} from './ProvideBreadcrumbs';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
interface Props {
location: Location;
breadcrumbs: Breadcrumb[];
core: AppMountContext['core'];
core: CoreStart;
}
function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) {
@ -27,15 +27,24 @@ function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) {
class UpdateBreadcrumbsComponent extends React.Component<Props> {
public updateHeaderBreadcrumbs() {
const { basePath } = this.props.core.http;
const breadcrumbs = this.props.breadcrumbs.map(
({ value, match }, index) => {
const { search } = this.props.location;
const isLastBreadcrumbItem =
index === this.props.breadcrumbs.length - 1;
const href = isLastBreadcrumbItem
? undefined // makes the breadcrumb item not clickable
: getAPMHref({ basePath, path: match.url, search });
return {
text: value,
href: isLastBreadcrumbItem
? undefined // makes the breadcrumb item not clickable
: getAPMHref(match.url, this.props.location.search),
href,
onClick: (event: MouseEvent<HTMLAnchorElement>) => {
if (href) {
event.preventDefault();
this.props.core.application.navigateToUrl(href);
}
},
};
}
);

View file

@ -38,14 +38,28 @@ interface RouteParams {
}
export const renderAsRedirectTo = (to: string) => {
return ({ location }: RouteComponentProps<RouteParams>) => (
<Redirect
to={{
...location,
pathname: to,
}}
/>
);
return ({ location }: RouteComponentProps<RouteParams>) => {
let resolvedUrl: URL | undefined;
// Redirect root URLs with a hash to support backward compatibility with URLs
// from before we switched to the non-hash platform history.
if (location.pathname === '' && location.hash.length > 0) {
// We just want the search and pathname so the host doesn't matter
resolvedUrl = new URL(location.hash.slice(1), 'http://localhost');
to = resolvedUrl.pathname;
}
return (
<Redirect
to={{
...location,
hash: '',
pathname: to,
search: resolvedUrl ? resolvedUrl.search : location.search,
}}
/>
);
};
};
export const routes: BreadcrumbRoute[] = [

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { routes } from './';
describe('routes', () => {
describe('/', () => {
const route = routes.find((r) => r.path === '/');
describe('with no hash path', () => {
it('redirects to /services', () => {
const location = { hash: '', pathname: '/', search: '' };
expect(
(route as any).render({ location } as any).props.to.pathname
).toEqual('/services');
});
});
describe('with a hash path', () => {
it('redirects to the hash path', () => {
const location = {
hash:
'#/services/opbeans-python/transactions/view?rangeFrom=now-24h&rangeTo=now&refreshInterval=10000&refreshPaused=false&traceId=d919c89dc7ca48d84b9dde1fef01d1f8&transactionId=1b542853d787ba7b&transactionName=GET%20opbeans.views.product_customers&transactionType=request&flyoutDetailTab=&waterfallItemId=1b542853d787ba7b',
pathname: '',
search: '',
};
expect(((route as any).render({ location }) as any).props.to).toEqual({
hash: '',
pathname: '/services/opbeans-python/transactions/view',
search:
'?rangeFrom=now-24h&rangeTo=now&refreshInterval=10000&refreshPaused=false&traceId=d919c89dc7ca48d84b9dde1fef01d1f8&transactionId=1b542853d787ba7b&transactionName=GET%20opbeans.views.product_customers&transactionType=request&flyoutDetailTab=&waterfallItemId=1b542853d787ba7b',
});
});
});
});
});

View file

@ -5,13 +5,14 @@
*/
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useFetcher } from '../../../../../hooks/useFetcher';
import { history } from '../../../../../utils/history';
import { toQuery } from '../../../../shared/Links/url_helpers';
import { Settings } from '../../../Settings';
import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurations/AgentConfigurationCreateEdit';
import { toQuery } from '../../../../shared/Links/url_helpers';
export function EditAgentConfigurationRouteHandler() {
const history = useHistory();
const { search } = history.location;
// typescript complains because `pageStop` does not exist in `APMQueryParams`
@ -40,6 +41,7 @@ export function EditAgentConfigurationRouteHandler() {
}
export function CreateAgentConfigurationRouteHandler() {
const history = useHistory();
const { search } = history.location;
// Ignoring here because we specifically DO NOT want to add the query params to the global route handler

View file

@ -4,33 +4,33 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import numeral from '@elastic/numeral';
import {
Axis,
BarSeries,
BrushEndListener,
Chart,
DARK_THEME,
LIGHT_THEME,
niceTimeFormatByDay,
ScaleType,
SeriesNameFn,
Settings,
timeFormatter,
} from '@elastic/charts';
import { DARK_THEME, LIGHT_THEME } from '@elastic/charts';
import { Position } from '@elastic/charts/dist/utils/commons';
import {
EUI_CHARTS_THEME_DARK,
EUI_CHARTS_THEME_LIGHT,
} from '@elastic/eui/dist/eui_charts_theme';
import numeral from '@elastic/numeral';
import moment from 'moment';
import { Position } from '@elastic/charts/dist/utils/commons';
import { I18LABELS } from '../translations';
import { history } from '../../../../utils/history';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { ChartWrapper } from '../ChartWrapper';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { ChartWrapper } from '../ChartWrapper';
import { I18LABELS } from '../translations';
interface Props {
data?: Array<Record<string, number | null>>;
@ -38,6 +38,7 @@ interface Props {
}
export function PageViewsChart({ data, loading }: Props) {
const history = useHistory();
const { urlParams } = useUrlParams();
const { start, end } = urlParams;

View file

@ -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 lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { render } from '@testing-library/react';
import cytoscape from 'cytoscape';
import React, { ReactNode } from 'react';
import { ThemeContext } from 'styled-components';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
import { Controls } from './Controls';
import { CytoscapeContext } from './Cytoscape';
const cy = cytoscape({
elements: [{ classes: 'primary', data: { id: 'test node' } }],
});
function Wrapper({ children }: { children?: ReactNode }) {
return (
<CytoscapeContext.Provider value={cy}>
<MockApmPluginContextWrapper>
<ThemeContext.Provider value={{ eui: lightTheme }}>
{children}
</ThemeContext.Provider>
</MockApmPluginContextWrapper>
</CytoscapeContext.Provider>
);
}
describe('Controls', () => {
describe('with a primary node', () => {
it('links to the full map', async () => {
const result = render(<Controls />, { wrapper: Wrapper });
const { findByTestId } = result;
const button = await findByTestId('viewFullMapButton');
expect(button.getAttribute('href')).toEqual(
'/basepath/app/apm/service-map'
);
});
});
});

View file

@ -4,16 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext, useEffect, useState } from 'react';
import { EuiButtonIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { useTheme } from '../../../hooks/useTheme';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { getAPMHref } from '../../shared/Links/apm/APMLink';
import { APMQueryParams } from '../../shared/Links/url_helpers';
import { CytoscapeContext } from './Cytoscape';
import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions';
import { getAPMHref } from '../../shared/Links/apm/APMLink';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { APMQueryParams } from '../../shared/Links/url_helpers';
import { useTheme } from '../../../hooks/useTheme';
const ControlsContainer = styled('div')`
left: ${({ theme }) => theme.eui.gutterTypes.gutterMedium};
@ -96,6 +97,8 @@ function useDebugDownloadUrl(cy?: cytoscape.Core) {
}
export function Controls() {
const { core } = useApmPluginContext();
const { basePath } = core.http;
const theme = useTheme();
const cy = useContext(CytoscapeContext);
const { urlParams } = useUrlParams();
@ -103,6 +106,12 @@ export function Controls() {
const [zoom, setZoom] = useState((cy && cy.zoom()) || 1);
const duration = parseInt(theme.eui.euiAnimSpeedFast, 10);
const downloadUrl = useDebugDownloadUrl(cy);
const viewFullMapUrl = getAPMHref({
basePath,
path: '/service-map',
search: currentSearch,
query: urlParams as APMQueryParams,
});
// Handle zoom events
useEffect(() => {
@ -209,11 +218,8 @@ export function Controls() {
<Button
aria-label={viewFullMapLabel}
color="text"
href={getAPMHref(
'/service-map',
currentSearch,
urlParams as APMQueryParams
)}
data-test-subj="viewFullMapButton"
href={viewFullMapUrl}
iconType="apps"
/>
</EuiToolTip>

View file

@ -4,14 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { ReactNode } from 'react';
import { Buttons } from './Buttons';
import { render } from '@testing-library/react';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
function Wrapper({ children }: { children?: ReactNode }) {
return <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>;
}
describe('Popover Buttons', () => {
it('renders', () => {
expect(() =>
render(<Buttons selectedNodeServiceName="test service name" />)
render(<Buttons selectedNodeServiceName="test service name" />, {
wrapper: Wrapper,
})
).not.toThrowError();
});
@ -21,7 +28,8 @@ describe('Popover Buttons', () => {
<Buttons
onFocusClick={onFocusClick}
selectedNodeServiceName="test service name"
/>
/>,
{ wrapper: Wrapper }
);
const focusButton = await result.findByText('Focus map');

View file

@ -9,6 +9,7 @@
import { EuiButton, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { MouseEvent } from 'react';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { getAPMHref } from '../../../shared/Links/apm/APMLink';
import { APMQueryParams } from '../../../shared/Links/url_helpers';
@ -22,22 +23,24 @@ export function Buttons({
onFocusClick = () => {},
selectedNodeServiceName,
}: ButtonsProps) {
const { core } = useApmPluginContext();
const { basePath } = core.http;
// The params may contain the service name. We want to use the selected node's
// service name in the button URLs, so make a copy and set the
// `serviceName` property.
const urlParams = { ...useUrlParams().urlParams } as APMQueryParams;
urlParams.serviceName = selectedNodeServiceName;
const detailsUrl = getAPMHref(
`/services/${selectedNodeServiceName}/transactions`,
'',
urlParams
);
const focusUrl = getAPMHref(
`/services/${selectedNodeServiceName}/service-map`,
'',
urlParams
);
const detailsUrl = getAPMHref({
basePath,
path: `/services/${selectedNodeServiceName}/transactions`,
query: urlParams,
});
const focusUrl = getAPMHref({
basePath,
path: `/services/${selectedNodeServiceName}/service-map`,
query: urlParams,
});
return (
<>

View file

@ -5,11 +5,17 @@
*/
import { render } from '@testing-library/react';
import { CoreStart } from 'kibana/public';
import React, { ReactNode } from 'react';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import { License } from '../../../../../licensing/common/license';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
import { LicenseContext } from '../../../context/LicenseContext';
import { ServiceMap } from './';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiStats: () => {} },
} as Partial<CoreStart>);
const expiredLicense = new License({
signature: 'test signature',
@ -24,9 +30,11 @@ const expiredLicense = new License({
function Wrapper({ children }: { children?: ReactNode }) {
return (
<LicenseContext.Provider value={expiredLicense}>
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
</LicenseContext.Provider>
<KibanaReactContext.Provider>
<LicenseContext.Provider value={expiredLicense}>
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
</LicenseContext.Provider>
</KibanaReactContext.Provider>
);
}

View file

@ -4,43 +4,49 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { ReactChild, FunctionComponent } from 'react';
import { render, wait, waitForElement } from '@testing-library/react';
import { CoreStart } from 'kibana/public';
import React, { FunctionComponent, ReactChild } from 'react';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import { ServiceOverview } from '..';
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock';
import { ApmPluginContextValue } from '../../../../context/ApmPluginContext';
import {
MockApmPluginContextWrapper,
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters';
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock';
jest.mock('ui/new_platform');
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiStats: () => {} },
} as Partial<CoreStart>);
function wrapper({ children }: { children: ReactChild }) {
return (
<MockApmPluginContextWrapper
value={
({
...mockApmPluginContextValue,
core: {
...mockApmPluginContextValue.core,
http: { ...mockApmPluginContextValue.core.http, get: httpGet },
notifications: {
...mockApmPluginContextValue.core.notifications,
toasts: {
...mockApmPluginContextValue.core.notifications.toasts,
addWarning,
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper
value={
({
...mockApmPluginContextValue,
core: {
...mockApmPluginContextValue.core,
http: { ...mockApmPluginContextValue.core.http, get: httpGet },
notifications: {
...mockApmPluginContextValue.core.notifications,
toasts: {
...mockApmPluginContextValue.core.notifications.toasts,
addWarning,
},
},
},
},
} as unknown) as ApmPluginContextValue
}
>
{children}
</MockApmPluginContextWrapper>
} as unknown) as ApmPluginContextValue
}
>
{children}
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
);
}

View file

@ -156,7 +156,7 @@ NodeList [
>
<a
class="euiLink euiLink--primary c0"
href="#/services/My Go Service/transactions?"
href="/basepath/app/apm/services/My Go Service/transactions"
rel="noreferrer"
>
My Go Service
@ -262,7 +262,7 @@ NodeList [
>
<a
class="euiLink euiLink--primary c0"
href="#/services/My Python Service/transactions?"
href="/basepath/app/apm/services/My Python Service/transactions"
rel="noreferrer"
>
My Python Service

View file

@ -5,37 +5,37 @@
*/
import {
EuiBottomBar,
EuiButton,
EuiForm,
EuiTitle,
EuiSpacer,
EuiPanel,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiStat,
EuiBottomBar,
EuiText,
EuiForm,
EuiHealth,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiStat,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty } from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
import { FETCH_STATUS } from '../../../../../../hooks/useFetcher';
import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent';
import { history } from '../../../../../../utils/history';
import React, { useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useUiTracker } from '../../../../../../../../observability/public';
import { getOptionLabel } from '../../../../../../../common/agent_configuration/all_option';
import { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types';
import {
filterByAgent,
settingDefinitions,
validateSetting,
} from '../../../../../../../common/agent_configuration/setting_definitions';
import { saveConfig } from './saveConfig';
import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent';
import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext';
import { useUiTracker } from '../../../../../../../../observability/public';
import { FETCH_STATUS } from '../../../../../../hooks/useFetcher';
import { saveConfig } from './saveConfig';
import { SettingFormRow } from './SettingFormRow';
import { getOptionLabel } from '../../../../../../../common/agent_configuration/all_option';
function removeEmpty(obj: { [key: string]: any }) {
return Object.fromEntries(
@ -60,6 +60,7 @@ export function SettingsPage({
isEditMode: boolean;
onClickEdit: () => void;
}) {
const history = useHistory();
// get a telemetry UI event tracker
const trackApmEvent = useUiTracker({ app: 'apm' });
const { toasts } = useApmPluginContext().core.notifications;

View file

@ -4,19 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEmpty } from 'lodash';
import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import React, { useState, useEffect, useCallback } from 'react';
import { EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FetcherResult } from '../../../../../hooks/useFetcher';
import { history } from '../../../../../utils/history';
import { History } from 'history';
import { isEmpty } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
AgentConfigurationIntake,
AgentConfiguration,
AgentConfigurationIntake,
} from '../../../../../../common/agent_configuration/configuration_types';
import { FetcherResult } from '../../../../../hooks/useFetcher';
import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers';
import { ServicePage } from './ServicePage/ServicePage';
import { SettingsPage } from './SettingsPage/SettingsPage';
import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers';
type PageStep = 'choose-service-step' | 'choose-settings-step' | 'review-step';
@ -30,7 +31,7 @@ function getInitialNewConfig(
};
}
function setPage(pageStep: PageStep) {
function setPage(pageStep: PageStep, history: History) {
history.push({
...history.location,
search: fromQuery({
@ -68,6 +69,7 @@ export function AgentConfigurationCreateEdit({
pageStep: PageStep;
existingConfigResult?: FetcherResult<AgentConfiguration>;
}) {
const history = useHistory();
const existingConfig = existingConfigResult?.data;
const isEditMode = Boolean(existingConfigResult);
const [newConfig, setNewConfig] = useState<AgentConfigurationIntake>(
@ -90,7 +92,7 @@ export function AgentConfigurationCreateEdit({
useEffect(() => {
// the user tried to edit the service of an existing config
if (pageStep === 'choose-service-step' && isEditMode) {
setPage('choose-settings-step');
setPage('choose-settings-step', history);
}
// the user skipped the first step (select service)
@ -99,9 +101,9 @@ export function AgentConfigurationCreateEdit({
!isEditMode &&
isEmpty(newConfig.service)
) {
setPage('choose-service-step');
setPage('choose-service-step', history);
}
}, [isEditMode, newConfig, pageStep]);
}, [history, isEditMode, newConfig, pageStep]);
const unsavedChanges = getUnsavedChanges({ newConfig, existingConfig });
@ -135,7 +137,7 @@ export function AgentConfigurationCreateEdit({
setNewConfig={setNewConfig}
onClickNext={() => {
resetSettings();
setPage('choose-settings-step');
setPage('choose-settings-step', history);
}}
/>
)}
@ -144,7 +146,7 @@ export function AgentConfigurationCreateEdit({
<SettingsPage
status={existingConfigResult?.status}
unsavedChanges={unsavedChanges}
onClickEdit={() => setPage('choose-service-step')}
onClickEdit={() => setPage('choose-service-step', history)}
newConfig={newConfig}
setNewConfig={setNewConfig}
resetSettings={resetSettings}

View file

@ -4,30 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiEmptyPrompt,
EuiButton,
EuiButtonEmpty,
EuiButtonIcon,
EuiEmptyPrompt,
EuiHealth,
EuiToolTip,
EuiButtonIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { useTheme } from '../../../../../hooks/useTheme';
import { FETCH_STATUS } from '../../../../../hooks/useFetcher';
import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt';
import React, { useState } from 'react';
import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations';
import { TimestampTooltip } from '../../../../shared/TimestampTooltip';
import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext';
import { FETCH_STATUS } from '../../../../../hooks/useFetcher';
import { useLocation } from '../../../../../hooks/useLocation';
import { useTheme } from '../../../../../hooks/useTheme';
import { px, units } from '../../../../../style/variables';
import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option';
import {
createAgentConfigurationHref,
editAgentConfigurationHref,
} from '../../../../shared/Links/apm/agentConfigurationLinks';
import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt';
import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable';
import { TimestampTooltip } from '../../../../shared/TimestampTooltip';
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
type Config = AgentConfigurationListAPIResponse[0];
@ -39,6 +41,9 @@ interface Props {
}
export function AgentConfigurationList({ status, data, refetch }: Props) {
const { core } = useApmPluginContext();
const { basePath } = core.http;
const { search } = useLocation();
const theme = useTheme();
const [configToBeDeleted, setConfigToBeDeleted] = useState<Config | null>(
null
@ -69,7 +74,11 @@ export function AgentConfigurationList({ status, data, refetch }: Props) {
</>
}
actions={
<EuiButton color="primary" fill href={createAgentConfigurationHref()}>
<EuiButton
color="primary"
fill
href={createAgentConfigurationHref(search, basePath)}
>
{i18n.translate(
'xpack.apm.agentConfig.configTable.createConfigButtonLabel',
{ defaultMessage: 'Create configuration' }
@ -145,7 +154,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) {
flush="left"
size="s"
color="primary"
href={editAgentConfigurationHref(config.service)}
href={editAgentConfigurationHref(config.service, search, basePath)}
>
{getOptionLabel(config.service.name)}
</EuiButtonEmpty>
@ -179,7 +188,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) {
<EuiButtonIcon
aria-label="Edit"
iconType="pencil"
href={editAgentConfigurationHref(config.service)}
href={editAgentConfigurationHref(config.service, search, basePath)}
/>
),
},

View file

@ -4,21 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiTitle,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiButton,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { useFetcher } from '../../../../hooks/useFetcher';
import { AgentConfigurationList } from './List';
import React from 'react';
import { useTrackPageview } from '../../../../../../observability/public';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { useFetcher } from '../../../../hooks/useFetcher';
import { useLocation } from '../../../../hooks/useLocation';
import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks';
import { AgentConfigurationList } from './List';
export function AgentConfigurations() {
const { refetch, data = [], status } = useFetcher(
@ -60,7 +62,10 @@ export function AgentConfigurations() {
}
function CreateConfigurationButton() {
const href = createAgentConfigurationHref();
const { core } = useApmPluginContext();
const { basePath } = core.http;
const { search } = useLocation();
const href = createAgentConfigurationHref(search, basePath);
return (
<EuiFlexItem>
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { render } from '@testing-library/react';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
import React, { ReactNode } from 'react';
import { Settings } from './';
import { LocationContext } from '../../../context/LocationContext';
import { createMemoryHistory } from 'history';
function Wrapper({ children }: { children?: ReactNode }) {
const { location } = createMemoryHistory();
return (
<LocationContext.Provider value={location}>
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
</LocationContext.Provider>
);
}
describe('Settings', () => {
it('renders', async () => {
expect(() =>
render(
<Settings>
<div />
</Settings>,
{ wrapper: Wrapper }
)
).not.toThrowError();
});
});

View file

@ -19,9 +19,15 @@ import { getAPMHref } from '../../shared/Links/apm/APMLink';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
export function Settings(props: { children: ReactNode }) {
const plugin = useApmPluginContext();
const canAccessML = !!plugin.core.application.capabilities.ml?.canAccessML;
const { core } = useApmPluginContext();
const { basePath } = core.http;
const canAccessML = !!core.application.capabilities.ml?.canAccessML;
const { search, pathname } = useLocation();
function getSettingsHref(path: string) {
return getAPMHref({ basePath, path: `/settings${path}`, search });
}
return (
<>
<HomeLink>
@ -46,7 +52,7 @@ export function Settings(props: { children: ReactNode }) {
defaultMessage: 'Agent Configuration',
}),
id: '1',
href: getAPMHref('/settings/agent-configuration', search),
href: getSettingsHref('/agent-configuration'),
isSelected: pathname.startsWith(
'/settings/agent-configuration'
),
@ -61,10 +67,7 @@ export function Settings(props: { children: ReactNode }) {
}
),
id: '4',
href: getAPMHref(
'/settings/anomaly-detection',
search
),
href: getSettingsHref('/anomaly-detection'),
isSelected:
pathname === '/settings/anomaly-detection',
},
@ -75,7 +78,7 @@ export function Settings(props: { children: ReactNode }) {
defaultMessage: 'Customize app',
}),
id: '3',
href: getAPMHref('/settings/customize-ui', search),
href: getSettingsHref('/customize-ui'),
isSelected: pathname === '/settings/customize-ui',
},
{
@ -83,7 +86,7 @@ export function Settings(props: { children: ReactNode }) {
defaultMessage: 'Indices',
}),
id: '2',
href: getAPMHref('/settings/apm-indices', search),
href: getSettingsHref('/apm-indices'),
isSelected: pathname === '/settings/apm-indices',
},
],

View file

@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { isEmpty } from 'lodash';
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { getDurationFormatter } from '../../../../utils/formatters';
import { history } from '../../../../utils/history';
// @ts-ignore
import Histogram from '../../../shared/charts/Histogram';
import { EmptyMessage } from '../../../shared/EmptyMessage';
@ -106,6 +106,7 @@ export function TransactionDistribution(props: Props) {
isLoading,
bucketIndex,
} = props;
const history = useHistory();
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const formatYShort = useCallback(getFormatYShort(transactionType), [

View file

@ -8,10 +8,10 @@ import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
import { TransactionMetadata } from '../../../shared/MetadataTable/TransactionMetadata';
import { WaterfallContainer } from './WaterfallContainer';
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
@ -45,6 +45,7 @@ export function TransactionTabs({
waterfall,
exceedsMax,
}: Props) {
const history = useHistory();
const tabs = [timelineTab, metadataTab];
const currentTab =
urlParams.detailTab === metadataTab.key ? metadataTab : timelineTab;

View file

@ -3,8 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Location } from 'history';
import { History, Location } from 'history';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { SpanFlyout } from './SpanFlyout';
import { TransactionFlyout } from './TransactionFlyout';
import { IWaterfall } from './waterfall_helpers/waterfall_helpers';
@ -13,7 +14,13 @@ interface Props {
waterfallItemId?: string;
waterfall: IWaterfall;
location: Location;
toggleFlyout: ({ location }: { location: Location }) => void;
toggleFlyout: ({
history,
location,
}: {
history: History;
location: Location;
}) => void;
}
export function WaterfallFlyout({
@ -22,6 +29,7 @@ export function WaterfallFlyout({
location,
toggleFlyout,
}: Props) {
const history = useHistory();
const currentItem = waterfall.items.find(
(item) => item.id === waterfallItemId
);
@ -42,14 +50,14 @@ export function WaterfallFlyout({
totalDuration={waterfall.duration}
span={currentItem.doc}
parentTransaction={parentTransaction}
onClose={() => toggleFlyout({ location })}
onClose={() => toggleFlyout({ history, location })}
/>
);
case 'transaction':
return (
<TransactionFlyout
transaction={currentItem.doc}
onClose={() => toggleFlyout({ location })}
onClose={() => toggleFlyout({ history, location })}
rootTransactionDuration={
waterfall.rootTransaction?.transaction.duration.us
}

View file

@ -6,13 +6,13 @@
import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import { History, Location } from 'history';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
// @ts-ignore
import { StickyContainer } from 'react-sticky';
import styled from 'styled-components';
import { px } from '../../../../../../style/variables';
import { history } from '../../../../../../utils/history';
import { Timeline } from '../../../../../shared/charts/Timeline';
import { HeightRetainer } from '../../../../../shared/HeightRetainer';
import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers';
@ -39,9 +39,11 @@ const TIMELINE_MARGINS = {
};
const toggleFlyout = ({
history,
item,
location,
}: {
history: History;
item?: IWaterfallItem;
location: Location;
}) => {
@ -74,6 +76,7 @@ export function Waterfall({
waterfallItemId,
location,
}: Props) {
const history = useHistory();
const [isAccordionOpen, setIsAccordionOpen] = useState(true);
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
const waterfallHeight = itemContainerHeight * waterfall.items.length;
@ -105,7 +108,7 @@ export function Waterfall({
childrenByParentId={childrenByParentId}
timelineMargins={TIMELINE_MARGINS}
onClickWaterfallItem={(item: IWaterfallItem) =>
toggleFlyout({ item, location })
toggleFlyout({ history, item, location })
}
/>
);

View file

@ -16,10 +16,10 @@ import {
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { history } from '../../../../utils/history';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { TransactionSummary } from '../../../shared/Summary/TransactionSummary';
@ -45,6 +45,7 @@ export function WaterfallWithSummmary({
isLoading,
traceSamples,
}: Props) {
const history = useHistory();
const [sampleActivePage, setSampleActivePage] = useState(0);
useEffect(() => {

View file

@ -10,22 +10,29 @@ import {
queryByLabelText,
render,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
import { omit } from 'lodash';
import React from 'react';
import { Router } from 'react-router-dom';
import { TransactionOverview } from './';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
import { UrlParamsProvider } from '../../../context/UrlParamsContext';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import * as useFetcherHook from '../../../hooks/useFetcher';
import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes';
import { history } from '../../../utils/history';
import { disableConsoleWarning } from '../../../utils/testHelpers';
import { fromQuery } from '../../shared/Links/url_helpers';
import { TransactionOverview } from './';
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiStats: () => {} },
} as Partial<CoreStart>);
const history = createMemoryHistory();
jest.spyOn(history, 'push');
jest.spyOn(history, 'replace');
jest.mock('ui/new_platform');
function setup({
urlParams,
serviceTransactionTypes,
@ -49,17 +56,29 @@ function setup({
jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any);
return render(
<MockApmPluginContextWrapper>
<Router history={history}>
<UrlParamsProvider>
<TransactionOverview />
</UrlParamsProvider>
</Router>
</MockApmPluginContextWrapper>
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper>
<Router history={history}>
<UrlParamsProvider>
<TransactionOverview />
</UrlParamsProvider>
</Router>
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
);
}
describe('TransactionOverview', () => {
let consoleMock: jest.SpyInstance;
beforeAll(() => {
consoleMock = disableConsoleWarning('Warning: componentWillReceiveProps');
});
afterAll(() => {
consoleMock.mockRestore();
});
afterEach(() => {
jest.clearAllMocks();
});

View file

@ -5,40 +5,39 @@
*/
import {
EuiPanel,
EuiSpacer,
EuiTitle,
EuiCallOut,
EuiCode,
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiCallOut,
EuiCode,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { Location } from 'history';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Location } from 'history';
import { first } from 'lodash';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGrid } from '@elastic/eui';
import { useTransactionList } from '../../../hooks/useTransactionList';
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart';
import { TransactionBreakdown } from '../../shared/TransactionBreakdown';
import { TransactionList } from './List';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { useRedirect } from './useRedirect';
import { history } from '../../../utils/history';
import { useLocation } from '../../../hooks/useLocation';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { useLocation } from '../../../hooks/useLocation';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
import { useTransactionList } from '../../../hooks/useTransactionList';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart';
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { Projection } from '../../../../common/projections';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter';
import { TransactionBreakdown } from '../../shared/TransactionBreakdown';
import { TransactionList } from './List';
import { useRedirect } from './useRedirect';
function getRedirectLocation({
urlParams,
@ -73,7 +72,6 @@ export function TransactionOverview() {
// redirect to first transaction type
useRedirect(
history,
getRedirectLocation({
urlParams,
location,

View file

@ -4,13 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { History, Location } from 'history';
import { Location } from 'history';
import { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
export function useRedirect(redirectLocation?: Location) {
const history = useHistory();
export function useRedirect(history: History, redirectLocation?: Location) {
useEffect(() => {
if (redirectLocation) {
history.replace(redirectLocation);
}
});
}, [history, redirectLocation]);
}

View file

@ -4,21 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSuperDatePicker } from '@elastic/eui';
import { wait } from '@testing-library/react';
import { mount } from 'enzyme';
import { createMemoryHistory } from 'history';
import React, { ReactNode } from 'react';
import { Router } from 'react-router-dom';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { LocationProvider } from '../../../../context/LocationContext';
import {
UrlParamsContext,
useUiFilters,
} from '../../../../context/UrlParamsContext';
import { DatePicker } from '../index';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { history } from '../../../../utils/history';
import { mount } from 'enzyme';
import { EuiSuperDatePicker } from '@elastic/eui';
import { MemoryRouter } from 'react-router-dom';
import { wait } from '@testing-library/react';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { DatePicker } from '../index';
const history = createMemoryHistory();
const mockHistoryPush = jest.spyOn(history, 'push');
const mockHistoryReplace = jest.spyOn(history, 'replace');
const mockRefreshTimeRange = jest.fn();
@ -44,13 +45,13 @@ function MockUrlParamsProvider({
function mountDatePicker(params?: IUrlParams) {
return mount(
<MockApmPluginContextWrapper>
<MemoryRouter initialEntries={[history.location]}>
<Router history={history}>
<LocationProvider>
<MockUrlParamsProvider params={params}>
<DatePicker />
</MockUrlParamsProvider>
</LocationProvider>
</MemoryRouter>
</Router>
</MockApmPluginContextWrapper>
);
}

View file

@ -5,15 +5,15 @@
*/
import { EuiSuperDatePicker } from '@elastic/eui';
import React from 'react';
import { isEmpty, isEqual, pickBy } from 'lodash';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { history } from '../../../utils/history';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { clearCache } from '../../../services/rest/callApi';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings';
function removeUndefinedAndEmptyProps<T extends object>(obj: T): Partial<T> {
@ -21,6 +21,7 @@ function removeUndefinedAndEmptyProps<T extends object>(obj: T): Partial<T> {
}
export function DatePicker() {
const history = useHistory();
const location = useLocation();
const { core } = useApmPluginContext();

View file

@ -6,18 +6,20 @@
import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
import React from 'react';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { history } from '../../../utils/history';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { useHistory } from 'react-router-dom';
import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
} from '../../../../common/environment_filter_values';
import { useEnvironments } from '../../../hooks/useEnvironments';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { fromQuery, toQuery } from '../Links/url_helpers';
function updateEnvironmentUrl(
history: History,
location: ReturnType<typeof useLocation>,
environment?: string
) {
@ -59,6 +61,7 @@ function getOptions(environments: string[]) {
}
export function EnvironmentFilter() {
const history = useHistory();
const location = useLocation();
const { uiFilters, urlParams } = useUrlParams();
@ -78,7 +81,7 @@ export function EnvironmentFilter() {
options={getOptions(environments)}
value={environment || ENVIRONMENT_ALL.value}
onChange={(event) => {
updateEnvironmentUrl(location, event.target.value);
updateEnvironmentUrl(history, location, event.target.value);
}}
isLoading={status === 'loading'}
/>

View file

@ -4,24 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { uniqueId, startsWith } from 'lodash';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { fromQuery, toQuery } from '../Links/url_helpers';
// @ts-expect-error
import { Typeahead } from './Typeahead';
import { getBoolFilter } from './get_bool_filter';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { history } from '../../../utils/history';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern';
import { startsWith, uniqueId } from 'lodash';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
import {
QuerySuggestion,
esKuery,
IIndexPattern,
QuerySuggestion,
} from '../../../../../../../src/plugins/data/public';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { getBoolFilter } from './get_bool_filter';
// @ts-expect-error
import { Typeahead } from './Typeahead';
const Container = styled.div`
margin-bottom: 10px;
@ -38,6 +38,7 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
}
export function KueryBar() {
const history = useHistory();
const [state, setState] = useState<State>({
suggestions: [],
isLoadingSuggestions: false,

View file

@ -6,14 +6,14 @@
import { EuiLink } from '@elastic/eui';
import { Location } from 'history';
import { IBasePath } from 'kibana/public';
import React from 'react';
import url from 'url';
import rison, { RisonValue } from 'rison-node';
import { useLocation } from '../../../../hooks/useLocation';
import { getTimepickerRisonData } from '../rison_helpers';
import url from 'url';
import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { AppMountContextBasePath } from '../../../../context/ApmPluginContext';
import { useLocation } from '../../../../hooks/useLocation';
import { getTimepickerRisonData } from '../rison_helpers';
interface Props {
query: {
@ -37,7 +37,7 @@ export const getDiscoverHref = ({
location,
query,
}: {
basePath: AppMountContextBasePath;
basePath: IBasePath;
location: Location;
query: Props['query'];
}) => {

View file

@ -5,12 +5,12 @@
*/
import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
import { IBasePath } from 'kibana/public';
import React from 'react';
import url from 'url';
import { fromQuery } from './url_helpers';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { AppMountContextBasePath } from '../../../context/ApmPluginContext';
import { InfraAppId } from '../../../../../infra/public';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { fromQuery } from './url_helpers';
interface InfraQueryParams {
time?: number;
@ -33,7 +33,7 @@ export const getInfraHref = ({
path,
}: {
app: InfraAppId;
basePath: AppMountContextBasePath;
basePath: IBasePath;
query: InfraQueryParams;
path?: string;
}) => {

View file

@ -9,49 +9,51 @@ import React from 'react';
import { getRenderedHref } from '../../../../utils/testHelpers';
import { APMLink } from './APMLink';
test('APMLink should produce the correct URL', async () => {
const href = await getRenderedHref(
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
{
search:
'?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
} as Location
);
describe('APMLink', () => {
test('APMLink should produce the correct URL', async () => {
const href = await getRenderedHref(
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
{
search:
'?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
} as Location
);
expect(href).toMatchInlineSnapshot(
`"#/some/path?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
);
});
test('APMLink should retain current kuery value if it exists', async () => {
const href = await getRenderedHref(
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
{
search:
'?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
} as Location
);
expect(href).toMatchInlineSnapshot(
`"#/some/path?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
);
});
test('APMLink should overwrite current kuery value if new kuery value is provided', async () => {
const href = await getRenderedHref(
() => (
<APMLink
path="/some/path"
query={{ kuery: 'host.os~20~3A~20~22linux~22' }}
/>
),
{
search:
'?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
} as Location
);
expect(href).toMatchInlineSnapshot(
`"#/some/path?kuery=host.os~20~3A~20~22linux~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0"`
);
expect(href).toMatchInlineSnapshot(
`"/basepath/app/apm/some/path?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
);
});
test('APMLink should retain current kuery value if it exists', async () => {
const href = await getRenderedHref(
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
{
search:
'?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
} as Location
);
expect(href).toMatchInlineSnapshot(
`"/basepath/app/apm/some/path?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
);
});
test('APMLink should overwrite current kuery value if new kuery value is provided', async () => {
const href = await getRenderedHref(
() => (
<APMLink
path="/some/path"
query={{ kuery: 'host.os~20~3A~20~22linux~22' }}
/>
),
{
search:
'?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
} as Location
);
expect(href).toMatchInlineSnapshot(
`"/basepath/app/apm/some/path?kuery=host.os~20~3A~20~22linux~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0"`
);
});
});

View file

@ -5,11 +5,13 @@
*/
import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
import { IBasePath } from 'kibana/public';
import { pick } from 'lodash';
import React from 'react';
import url from 'url';
import { pick } from 'lodash';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { useLocation } from '../../../../hooks/useLocation';
import { APMQueryParams, toQuery, fromQuery } from '../url_helpers';
import { APMQueryParams, fromQuery, toQuery } from '../url_helpers';
interface Props extends EuiLinkAnchorProps {
path?: string;
@ -28,12 +30,21 @@ export const PERSISTENT_APM_PARAMS = [
'environment',
];
export function getAPMHref(
path: string,
currentSearch: string,
query: APMQueryParams = {}
) {
const currentQuery = toQuery(currentSearch);
/**
* Get an APM link for a path.
*/
export function getAPMHref({
basePath,
path = '',
search,
query = {},
}: {
basePath: IBasePath;
path?: string;
search?: string;
query?: APMQueryParams;
}) {
const currentQuery = toQuery(search);
const nextQuery = {
...pick(currentQuery, PERSISTENT_APM_PARAMS),
...query,
@ -41,13 +52,16 @@ export function getAPMHref(
const nextSearch = fromQuery(nextQuery);
return url.format({
pathname: '',
hash: `${path}?${nextSearch}`,
pathname: basePath.prepend(`/app/apm${path}`),
search: nextSearch,
});
}
export function APMLink({ path = '', query, ...rest }: Props) {
const { core } = useApmPluginContext();
const { search } = useLocation();
const href = getAPMHref(path, search, query);
const { basePath } = core.http;
const href = getAPMHref({ basePath, path, search, query });
return <EuiLink {...rest} href={href} />;
}

View file

@ -4,23 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getAPMHref } from './APMLink';
import { IBasePath } from 'kibana/public';
import { AgentConfigurationIntake } from '../../../../../common/agent_configuration/configuration_types';
import { history } from '../../../../utils/history';
import { getAPMHref } from './APMLink';
export function editAgentConfigurationHref(
configService: AgentConfigurationIntake['service']
configService: AgentConfigurationIntake['service'],
search: string,
basePath: IBasePath
) {
const { search } = history.location;
return getAPMHref('/settings/agent-configuration/edit', search, {
// ignoring because `name` has not been added to url params. Related: https://github.com/elastic/kibana/issues/51963
// @ts-ignore
name: configService.name,
environment: configService.environment,
return getAPMHref({
basePath,
path: '/settings/agent-configuration/edit',
search,
query: {
// ignoring because `name` has not been added to url params. Related: https://github.com/elastic/kibana/issues/51963
// @ts-expect-error
name: configService.name,
environment: configService.environment,
},
});
}
export function createAgentConfigurationHref() {
const { search } = history.location;
return getAPMHref('/settings/agent-configuration/create', search);
export function createAgentConfigurationHref(
search: string,
basePath: IBasePath
) {
return getAPMHref({
basePath,
path: '/settings/agent-configuration/create',
search,
});
}

View file

@ -4,16 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect } from 'react';
import {
EuiTitle,
EuiHorizontalRule,
EuiSpacer,
EuiSelect,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { history } from '../../../../utils/history';
import { fromQuery, toQuery } from '../../Links/url_helpers';
interface Props {
@ -22,6 +22,7 @@ interface Props {
}
function ServiceNameFilter({ loading, serviceNames }: Props) {
const history = useHistory();
const {
urlParams: { serviceName },
} = useUrlParams();
@ -31,22 +32,25 @@ function ServiceNameFilter({ loading, serviceNames }: Props) {
value: type,
}));
const updateServiceName = (serviceN: string) => {
const newLocation = {
...history.location,
search: fromQuery({
...toQuery(history.location.search),
serviceName: serviceN,
}),
};
history.push(newLocation);
};
const updateServiceName = useCallback(
(serviceN: string) => {
const newLocation = {
...history.location,
search: fromQuery({
...toQuery(history.location.search),
serviceName: serviceN,
}),
};
history.push(newLocation);
},
[history]
);
useEffect(() => {
if (!serviceName && serviceNames.length > 0) {
updateServiceName(serviceNames[0]);
}
}, [serviceNames, serviceName]);
}, [serviceNames, serviceName, updateServiceName]);
return (
<>

View file

@ -4,16 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiTitle,
EuiHorizontalRule,
EuiSpacer,
EuiRadioGroup,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { history } from '../../../../utils/history';
import { fromQuery, toQuery } from '../../Links/url_helpers';
interface Props {
@ -21,6 +21,7 @@ interface Props {
}
function TransactionTypeFilter({ transactionTypes }: Props) {
const history = useHistory();
const {
urlParams: { transactionType },
} = useUrlParams();

View file

@ -6,9 +6,9 @@
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { orderBy } from 'lodash';
import React, { useMemo, useCallback, ReactNode } from 'react';
import React, { ReactNode, useCallback, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { history } from '../../../utils/history';
import { fromQuery, toQuery } from '../Links/url_helpers';
// TODO: this should really be imported from EUI
@ -37,6 +37,7 @@ interface Props<T> {
}
function UnoptimizedManagedTable<T>(props: Props<T>) {
const history = useHistory();
const {
items,
columns,
@ -92,7 +93,7 @@ function UnoptimizedManagedTable<T>(props: Props<T>) {
}),
});
},
[]
[history]
);
const paginationProps = useMemo(() => {

View file

@ -5,31 +5,32 @@
*/
import {
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiText,
EuiTitle,
EuiFieldSearch,
} from '@elastic/eui';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { EuiText } from '@elastic/eui';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { HeightRetainer } from '../HeightRetainer';
import { Section } from './Section';
import { history } from '../../../utils/history';
import { fromQuery, toQuery } from '../Links/url_helpers';
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { SectionsWithRows, filterSectionsByTerm } from './helper';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { HeightRetainer } from '../HeightRetainer';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { filterSectionsByTerm, SectionsWithRows } from './helper';
import { Section } from './Section';
interface Props {
sections: SectionsWithRows;
}
export function MetadataTable({ sections }: Props) {
const history = useHistory();
const location = useLocation();
const { urlParams } = useUrlParams();
const { searchTerm = '' } = urlParams;
@ -47,7 +48,7 @@ export function MetadataTable({ sections }: Props) {
}),
});
},
[location]
[history, location]
);
const noResultFound = Boolean(searchTerm) && isEmpty(filteredSections);
return (

View file

@ -3,12 +3,17 @@
* 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, act, fireEvent } from '@testing-library/react';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { CustomLinkPopover } from './CustomLinkPopover';
import { expectTextsInDocument } from '../../../../utils/testHelpers';
import { act, fireEvent, render } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { CustomLink } from '../../../../../common/custom_link/custom_link_types';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { expectTextsInDocument } from '../../../../utils/testHelpers';
import { CustomLinkPopover } from './CustomLinkPopover';
function Wrapper({ children }: { children?: ReactNode }) {
return <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>;
}
describe('CustomLinkPopover', () => {
const customLinks = [
@ -29,7 +34,8 @@ describe('CustomLinkPopover', () => {
transaction={transaction}
onCreateCustomLinkClick={jest.fn()}
onClose={jest.fn()}
/>
/>,
{ wrapper: Wrapper }
);
expectTextsInDocument(component, ['CUSTOM LINKS', 'Create', 'foo', 'bar']);
});
@ -42,7 +48,8 @@ describe('CustomLinkPopover', () => {
transaction={transaction}
onCreateCustomLinkClick={jest.fn()}
onClose={handleCloseMock}
/>
/>,
{ wrapper: Wrapper }
);
expect(handleCloseMock).not.toHaveBeenCalled();
act(() => {
@ -59,7 +66,8 @@ describe('CustomLinkPopover', () => {
transaction={transaction}
onCreateCustomLinkClick={handleCreateCustomLinkClickMock}
onClose={jest.fn()}
/>
/>,
{ wrapper: Wrapper }
);
expect(handleCreateCustomLinkClickMock).not.toHaveBeenCalled();
act(() => {

View file

@ -4,18 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { ReactNode } from 'react';
import { render, act, fireEvent } from '@testing-library/react';
import { ManageCustomLink } from './ManageCustomLink';
import {
expectTextsInDocument,
expectTextsNotInDocument,
} from '../../../../utils/testHelpers';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
function Wrapper({ children }: { children?: ReactNode }) {
return <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>;
}
describe('ManageCustomLink', () => {
it('renders with create button', () => {
const component = render(
<ManageCustomLink onCreateCustomLinkClick={jest.fn()} />
<ManageCustomLink onCreateCustomLinkClick={jest.fn()} />,
{ wrapper: Wrapper }
);
expect(
component.getByLabelText('Custom links settings page')
@ -27,7 +33,8 @@ describe('ManageCustomLink', () => {
<ManageCustomLink
onCreateCustomLinkClick={jest.fn()}
showCreateCustomLinkButton={false}
/>
/>,
{ wrapper: Wrapper }
);
expect(
component.getByLabelText('Custom links settings page')
@ -39,7 +46,8 @@ describe('ManageCustomLink', () => {
const { getByText } = render(
<ManageCustomLink
onCreateCustomLinkClick={handleCreateCustomLinkClickMock}
/>
/>,
{ wrapper: Wrapper }
);
expect(handleCreateCustomLinkClickMock).not.toHaveBeenCalled();
act(() => {

View file

@ -4,16 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render, act, fireEvent } from '@testing-library/react';
import { act, fireEvent, render } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { CustomLink } from '.';
import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import {
expectTextsInDocument,
expectTextsNotInDocument,
} from '../../../../utils/testHelpers';
import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types';
function Wrapper({ children }: { children?: ReactNode }) {
return <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>;
}
describe('Custom links', () => {
it('shows empty message when no custom link is available', () => {
@ -24,7 +29,8 @@ describe('Custom links', () => {
onCreateCustomLinkClick={jest.fn()}
onSeeMoreClick={jest.fn()}
status={FETCH_STATUS.SUCCESS}
/>
/>,
{ wrapper: Wrapper }
);
expectTextsInDocument(component, [
@ -41,7 +47,8 @@ describe('Custom links', () => {
onCreateCustomLinkClick={jest.fn()}
onSeeMoreClick={jest.fn()}
status={FETCH_STATUS.LOADING}
/>
/>,
{ wrapper: Wrapper }
);
expect(getByTestId('loading-spinner')).toBeInTheDocument();
});
@ -60,7 +67,8 @@ describe('Custom links', () => {
onCreateCustomLinkClick={jest.fn()}
onSeeMoreClick={jest.fn()}
status={FETCH_STATUS.SUCCESS}
/>
/>,
{ wrapper: Wrapper }
);
expectTextsInDocument(component, ['foo', 'bar', 'baz']);
expectTextsNotInDocument(component, ['qux']);
@ -81,7 +89,8 @@ describe('Custom links', () => {
onCreateCustomLinkClick={jest.fn()}
onSeeMoreClick={onSeeMoreClickMock}
status={FETCH_STATUS.SUCCESS}
/>
/>,
{ wrapper: Wrapper }
);
expect(onSeeMoreClickMock).not.toHaveBeenCalled();
act(() => {
@ -99,7 +108,8 @@ describe('Custom links', () => {
onCreateCustomLinkClick={jest.fn()}
onSeeMoreClick={jest.fn()}
status={FETCH_STATUS.SUCCESS}
/>
/>,
{ wrapper: Wrapper }
);
expectTextsInDocument(component, ['Create custom link']);
@ -119,7 +129,8 @@ describe('Custom links', () => {
onCreateCustomLinkClick={jest.fn()}
onSeeMoreClick={jest.fn()}
status={FETCH_STATUS.SUCCESS}
/>
/>,
{ wrapper: Wrapper }
);
expectTextsInDocument(component, ['Create']);
expectTextsNotInDocument(component, ['Create custom link']);

View file

@ -6,8 +6,7 @@
import { EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { MouseEvent, useMemo, useState } from 'react';
import url from 'url';
import React, { useMemo, useState } from 'react';
import {
ActionMenu,
ActionMenuDivider,
@ -84,40 +83,7 @@ export function TransactionActionMenu({ transaction }: Props) {
basePath: core.http.basePath,
location,
urlParams,
}).map((sectionList) =>
sectionList.map((section) => ({
...section,
actions: section.actions.map((action) => {
const { href } = action;
// use navigateToApp as a temporary workaround for faster navigation between observability apps.
// see https://github.com/elastic/kibana/issues/65682
return {
...action,
onClick: (event: MouseEvent) => {
const parsed = url.parse(href);
const appPathname = core.http.basePath.remove(
parsed.pathname ?? ''
);
const [, , app, ...rest] = appPathname.split('/');
if (app === 'uptime' || app === 'metrics' || app === 'logs') {
event.preventDefault();
const search = parsed.search || '';
const path = `${rest.join('/')}${search}`;
core.application.navigateToApp(app, {
path,
});
}
},
};
}),
}))
);
});
const closePopover = () => {
setIsActionPopoverOpen(false);
@ -186,7 +152,6 @@ export function TransactionActionMenu({ transaction }: Props) {
key={action.key}
label={action.label}
href={action.href}
onClick={action.onClick}
/>
))}
</SectionLinks>

View file

@ -4,54 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { act, fireEvent, render } from '@testing-library/react';
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { merge, tail } from 'lodash';
import { TransactionActionMenu } from '../TransactionActionMenu';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import * as Transactions from './mockData';
import {
expectTextsNotInDocument,
expectTextsInDocument,
} from '../../../../utils/testHelpers';
import * as hooks from '../../../../hooks/useFetcher';
import { LicenseContext } from '../../../../context/LicenseContext';
import { License } from '../../../../../../licensing/common/license';
import {
MockApmPluginContextWrapper,
mockApmPluginContextValue,
} from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { LicenseContext } from '../../../../context/LicenseContext';
import * as hooks from '../../../../hooks/useFetcher';
import * as apmApi from '../../../../services/rest/createCallApmApi';
import { ApmPluginContextValue } from '../../../../context/ApmPluginContext';
import {
expectTextsInDocument,
expectTextsNotInDocument,
} from '../../../../utils/testHelpers';
import { TransactionActionMenu } from '../TransactionActionMenu';
import * as Transactions from './mockData';
const getMock = () => {
return (merge({}, mockApmPluginContextValue, {
core: {
application: {
navigateToApp: jest.fn(),
},
http: {
basePath: {
remove: jest.fn((path: string) => {
return tail(path.split('/')).join('/');
}),
},
},
},
}) as unknown) as ApmPluginContextValue;
};
const renderTransaction = async (
transaction: Record<string, any>,
mock: ApmPluginContextValue = getMock()
) => {
const renderTransaction = async (transaction: Record<string, any>) => {
const rendered = render(
<TransactionActionMenu transaction={transaction as Transaction} />,
{
wrapper: ({ children }: { children?: React.ReactNode }) => (
<MockApmPluginContextWrapper value={mock}>
{children}
</MockApmPluginContextWrapper>
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
),
}
);
@ -80,138 +53,148 @@ describe('TransactionActionMenu component', () => {
expect(queryByText('View sample document')).not.toBeNull();
});
it('should always render the trace logs link', async () => {
const mock = getMock();
const { queryByText, getByText } = await renderTransaction(
Transactions.transactionWithMinimalData,
mock
);
expect(queryByText('Trace logs')).not.toBeNull();
fireEvent.click(getByText('Trace logs'));
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', {
path:
'link-to/logs?time=1545092070952&filter=trace.id:%228b60bd32ecc6e1506735a8b6cfcf175c%22%20OR%20%228b60bd32ecc6e1506735a8b6cfcf175c%22',
});
});
it('should not render the pod links when there is no pod id', async () => {
const { queryByText } = await renderTransaction(
it('always renders the trace logs link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
expect(queryByText('Pod logs')).toBeNull();
expect(queryByText('Pod metrics')).toBeNull();
expect(
(getByText('Trace logs').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/logs/link-to/logs?time=1545092070952&filter=trace.id:%228b60bd32ecc6e1506735a8b6cfcf175c%22%20OR%20%228b60bd32ecc6e1506735a8b6cfcf175c%22'
);
});
it('should render the pod links when there is a pod id', async () => {
const mock = getMock();
describe('when there is no pod id', () => {
it('does not render the Pod logs link', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
const { queryByText, getByText } = await renderTransaction(
Transactions.transactionWithKubernetesData,
mock
);
expect(queryByText('Pod logs')).not.toBeNull();
expect(queryByText('Pod metrics')).not.toBeNull();
fireEvent.click(getByText('Pod logs'));
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', {
path: 'link-to/pod-logs/pod123456abcdef?time=1545092070952',
expect(queryByText('Pod logs')).toBeNull();
});
(mock.core.application.navigateToApp as jest.Mock).mockClear();
it('does not render the Pod metrics link', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
fireEvent.click(getByText('Pod metrics'));
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith(
'metrics',
{
path:
'link-to/pod-detail/pod123456abcdef?from=1545091770952&to=1545092370952',
}
);
expect(queryByText('Pod metrics')).toBeNull();
});
});
it('should not render the container links when there is no container id', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
describe('when there is a pod id', () => {
it('renders the pod logs link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithKubernetesData
);
expect(queryByText('Container logs')).toBeNull();
expect(queryByText('Container metrics')).toBeNull();
});
it('should render the container links when there is a container id', async () => {
const mock = getMock();
const { queryByText, getByText } = await renderTransaction(
Transactions.transactionWithContainerData,
mock
);
expect(queryByText('Container logs')).not.toBeNull();
expect(queryByText('Container metrics')).not.toBeNull();
fireEvent.click(getByText('Container logs'));
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', {
path: 'link-to/container-logs/container123456abcdef?time=1545092070952',
expect(
(getByText('Pod logs').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/logs/link-to/pod-logs/pod123456abcdef?time=1545092070952'
);
});
(mock.core.application.navigateToApp as jest.Mock).mockClear();
it('renders the pod metrics link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithKubernetesData
);
fireEvent.click(getByText('Container metrics'));
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith(
'metrics',
{
path:
'link-to/container-detail/container123456abcdef?from=1545091770952&to=1545092370952',
}
);
expect(
(getByText('Pod metrics').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/metrics/link-to/pod-detail/pod123456abcdef?from=1545091770952&to=1545092370952'
);
});
});
it('should not render the host links when there is no hostname', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
describe('when there is no container id', () => {
it('does not render the Container logs link', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
expect(queryByText('Host logs')).toBeNull();
expect(queryByText('Host metrics')).toBeNull();
});
it('should render the host links when there is a hostname', async () => {
const mock = getMock();
const { queryByText, getByText } = await renderTransaction(
Transactions.transactionWithHostData,
mock
);
expect(queryByText('Host logs')).not.toBeNull();
expect(queryByText('Host metrics')).not.toBeNull();
fireEvent.click(getByText('Host logs'));
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', {
path: 'link-to/host-logs/227453131a17?time=1545092070952',
expect(queryByText('Container logs')).toBeNull();
});
(mock.core.application.navigateToApp as jest.Mock).mockClear();
it('does not render the Container metrics link', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
fireEvent.click(getByText('Host metrics'));
expect(queryByText('Container metrics')).toBeNull();
});
});
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith(
'metrics',
{
path:
'link-to/host-detail/227453131a17?from=1545091770952&to=1545092370952',
}
);
describe('when there is a container id', () => {
it('renders the Container logs link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithContainerData
);
expect(
(getByText('Container logs').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/logs/link-to/container-logs/container123456abcdef?time=1545092070952'
);
});
it('renders the Container metrics link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithContainerData
);
expect(
(getByText('Container metrics').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/metrics/link-to/container-detail/container123456abcdef?from=1545091770952&to=1545092370952'
);
});
});
describe('when there is no hostname', () => {
it('does not render the Host logs link', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
expect(queryByText('Host logs')).toBeNull();
});
it('does not render the Host metrics link', async () => {
const { queryByText } = await renderTransaction(
Transactions.transactionWithMinimalData
);
expect(queryByText('Host metrics')).toBeNull();
});
});
describe('when there is a hostname', () => {
it('renders the Host logs link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithHostData
);
expect(
(getByText('Host logs').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/logs/link-to/host-logs/227453131a17?time=1545092070952'
);
});
it('renders the Host metrics link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithHostData
);
expect(
(getByText('Host metrics').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/metrics/link-to/host-detail/227453131a17?from=1545091770952&to=1545092370952'
);
});
});
it('should not render the uptime link if there is no url available', async () => {
@ -230,24 +213,21 @@ describe('TransactionActionMenu component', () => {
expect(queryByText('Status')).toBeNull();
});
it('should render the uptime link if there is a url with a domain', async () => {
const mock = getMock();
describe('when there is a url with a domain', () => {
it('renders the uptime link', async () => {
const { getByText } = await renderTransaction(
Transactions.transactionWithUrlAndDomain
);
const { queryByText, getByText } = await renderTransaction(
Transactions.transactionWithUrlAndDomain,
mock
);
expect(queryByText('Status')).not.toBeNull();
fireEvent.click(getByText('Status'));
expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('uptime', {
path: '?search=url.domain:%22example.com%22',
expect(
(getByText('Status').parentElement as HTMLAnchorElement).href
).toEqual(
'http://localhost/basepath/app/uptime?search=url.domain:%22example.com%22'
);
});
});
it('should match the snapshot', async () => {
it('matches the snapshot', async () => {
const { container } = await renderTransaction(
Transactions.transactionWithAllData
);

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TransactionActionMenu component should match the snapshot 1`] = `
exports[`TransactionActionMenu component matches the snapshot 1`] = `
<div>
<div
class="euiPopover euiPopover--anchorDownRight"

View file

@ -4,16 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Location } from 'history';
import { getSections } from '../sections';
import { IBasePath } from 'kibana/public';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { AppMountContextBasePath } from '../../../../context/ApmPluginContext';
import { getSections } from '../sections';
describe('Transaction action menu', () => {
const basePath = ({
prepend: (url: string) => {
return `some-basepath${url}`;
},
} as unknown) as AppMountContextBasePath;
} as unknown) as IBasePath;
const date = '2020-02-06T11:00:00.000Z';
const timestamp = { us: new Date(date).getTime() };

View file

@ -5,7 +5,8 @@
*/
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import { pickBy, isEmpty } from 'lodash';
import { IBasePath } from 'kibana/public';
import { isEmpty, pickBy } from 'lodash';
import moment from 'moment';
import url from 'url';
import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
@ -14,7 +15,6 @@ import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink';
import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink';
import { getInfraHref } from '../Links/InfraLink';
import { fromQuery } from '../Links/url_helpers';
import { AppMountContextBasePath } from '../../../context/ApmPluginContext';
function getInfraMetricsQuery(transaction: Transaction) {
const timestamp = new Date(transaction['@timestamp']).getTime();
@ -49,7 +49,7 @@ export const getSections = ({
urlParams,
}: {
transaction: Transaction;
basePath: AppMountContextBasePath;
basePath: IBasePath;
location: Location;
urlParams: IUrlParams;
}) => {

View file

@ -6,7 +6,11 @@
import moment from 'moment';
import React from 'react';
import { toJson, mountWithTheme } from '../../../../../utils/testHelpers';
import {
disableConsoleWarning,
toJson,
mountWithTheme,
} from '../../../../../utils/testHelpers';
import { InnerCustomPlot } from '../index';
import responseWithData from './responseWithData.json';
import VoronoiPlot from '../VoronoiPlot';
@ -19,11 +23,20 @@ function getXValueByIndex(index) {
}
describe('when response has data', () => {
let consoleMock;
let wrapper;
let onHover;
let onMouseLeave;
let onSelectionEnd;
beforeAll(() => {
consoleMock = disableConsoleWarning('Warning: componentWillReceiveProps');
});
afterAll(() => {
consoleMock.mockRestore();
});
beforeEach(() => {
const series = getResponseTimeSeries({ apmTimeseries: responseWithData });
onHover = jest.fn();

View file

@ -13,13 +13,27 @@ import {
getDurationFormatter,
asInteger,
} from '../../../../../utils/formatters';
import { toJson, mountWithTheme } from '../../../../../utils/testHelpers';
import {
disableConsoleWarning,
toJson,
mountWithTheme,
} from '../../../../../utils/testHelpers';
import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index';
describe('Histogram', () => {
let mockConsole;
let wrapper;
const onClick = jest.fn();
beforeAll(() => {
mockConsole = disableConsoleWarning('Warning: componentWillReceiveProps');
});
afterAll(() => {
mockConsole.mockRestore();
});
beforeEach(() => {
const buckets = getFormattedBuckets(response.buckets, response.bucketSize);
const xMax = d3.max(buckets, (d) => d.x);

View file

@ -4,15 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { ErrorMarker } from './ErrorMarker';
import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
import { fireEvent } from '@testing-library/react';
import { act } from '@testing-library/react-hooks';
import React, { ReactNode } from 'react';
import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext';
import {
expectTextsInDocument,
renderWithTheme,
} from '../../../../../utils/testHelpers';
import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
import { ErrorMarker } from './ErrorMarker';
function Wrapper({ children }: { children?: ReactNode }) {
return <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>;
}
describe('ErrorMarker', () => {
const mark = ({
@ -36,7 +41,9 @@ describe('ErrorMarker', () => {
} as unknown) as ErrorMark;
function openPopover(errorMark: ErrorMark) {
const component = renderWithTheme(<ErrorMarker mark={errorMark} />);
const component = renderWithTheme(<ErrorMarker mark={errorMark} />, {
wrapper: Wrapper,
});
act(() => {
fireEvent.click(component.getByTestId('popover'));
});
@ -51,7 +58,8 @@ describe('ErrorMarker', () => {
it('renders link with trace and transaction', () => {
const component = openPopover(mark);
const errorLink = component.getByTestId('errorLink') as HTMLAnchorElement;
expect(getKueryDecoded(errorLink.hash)).toEqual(
expect(getKueryDecoded(errorLink.search)).toEqual(
'kuery=trace.id : "123" and transaction.id : "456"'
);
});
@ -63,7 +71,7 @@ describe('ErrorMarker', () => {
} as ErrorMark;
const component = openPopover(newMark);
const errorLink = component.getByTestId('errorLink') as HTMLAnchorElement;
expect(getKueryDecoded(errorLink.hash)).toEqual('kuery=trace.id : "123"');
expect(getKueryDecoded(errorLink.search)).toEqual('kuery=trace.id : "123"');
});
it('renders link with transaction', () => {
const { trace, ...withoutTrace } = mark.error;
@ -73,7 +81,7 @@ describe('ErrorMarker', () => {
} as ErrorMark;
const component = openPopover(newMark);
const errorLink = component.getByTestId('errorLink') as HTMLAnchorElement;
expect(getKueryDecoded(errorLink.hash)).toEqual(
expect(getKueryDecoded(errorLink.search)).toEqual(
'kuery=transaction.id : "456"'
);
});
@ -85,7 +93,7 @@ describe('ErrorMarker', () => {
} as ErrorMark;
const component = openPopover(newMark);
const errorLink = component.getByTestId('errorLink') as HTMLAnchorElement;
expect(getKueryDecoded(errorLink.hash)).toEqual('kuery=');
expect(getKueryDecoded(errorLink.search)).toEqual('kuery=');
});
it('truncates the error message text', () => {
const { trace, transaction, ...withoutTraceAndTransaction } = mark.error;

View file

@ -7,6 +7,7 @@
import React from 'react';
import { StickyContainer } from 'react-sticky';
import {
disableConsoleWarning,
mountWithTheme,
mockMoment,
toJson,
@ -14,8 +15,15 @@ import {
import { Timeline } from '.';
describe('Timeline', () => {
let consoleMock: jest.SpyInstance;
beforeAll(() => {
mockMoment();
consoleMock = disableConsoleWarning('Warning: componentWill');
});
afterAll(() => {
consoleMock.mockRestore();
});
it('should render with data', () => {

View file

@ -3,11 +3,12 @@
* 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 React, { ReactNode } from 'react';
import { Observable, of } from 'rxjs';
import { ApmPluginContext, ApmPluginContextValue } from '.';
import { createCallApmApi } from '../../services/rest/createCallApmApi';
import { ConfigSchema } from '../..';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
import { createCallApmApi } from '../../services/rest/createCallApmApi';
const uiSettings: Record<string, unknown> = {
[UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [
@ -33,8 +34,17 @@ const uiSettings: Record<string, unknown> = {
};
const mockCore = {
application: {
capabilities: {
apm: {},
},
currentAppId$: new Observable(),
},
chrome: {
docTitle: { change: () => {} },
setBreadcrumbs: () => {},
setHelpExtension: () => {},
setBadge: () => {},
},
docLinks: {
DOC_LINK_VERSION: '0',
@ -45,6 +55,9 @@ const mockCore = {
prepend: (path: string) => `/basepath${path}`,
},
},
i18n: {
Context: ({ children }: { children: ReactNode }) => children,
},
notifications: {
toasts: {
addWarning: () => {},
@ -53,6 +66,7 @@ const mockCore = {
},
uiSettings: {
get: (key: string) => uiSettings[key],
get$: (key: string) => of(mockCore.uiSettings.get(key)),
},
};

View file

@ -4,16 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreStart } from 'kibana/public';
import { createContext } from 'react';
import { AppMountContext } from 'kibana/public';
import { ConfigSchema } from '../..';
import { ConfigSchema } from '../../';
import { ApmPluginSetupDeps } from '../../plugin';
export type AppMountContextBasePath = AppMountContext['core']['http']['basePath'];
export interface ApmPluginContextValue {
config: ConfigSchema;
core: AppMountContext['core'];
core: CoreStart;
plugins: ApmPluginSetupDeps;
}

View file

@ -5,10 +5,10 @@
*/
import React, { ReactNode, useMemo, useState } from 'react';
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
import { history } from '../utils/history';
import { useUrlParams } from '../hooks/useUrlParams';
import { useHistory } from 'react-router-dom';
import { fromQuery, toQuery } from '../components/shared/Links/url_helpers';
import { useFetcher } from '../hooks/useFetcher';
import { useUrlParams } from '../hooks/useUrlParams';
const ChartsSyncContext = React.createContext<{
hoverX: number | null;
@ -18,6 +18,7 @@ const ChartsSyncContext = React.createContext<{
} | null>(null);
function ChartsSyncContextProvider({ children }: { children: ReactNode }) {
const history = useHistory();
const [time, setTime] = useState<number | null>(null);
const { urlParams, uiFilters } = useUrlParams();
@ -75,7 +76,7 @@ function ChartsSyncContextProvider({ children }: { children: ReactNode }) {
};
return { ...hoverXHandlers };
}, [time, data.annotations]);
}, [history, time, data.annotations]);
return <ChartsSyncContext.Provider value={value} children={children} />;
}

View file

@ -5,21 +5,21 @@
*/
import { omit } from 'lodash';
import { useFetcher } from './useFetcher';
import { useHistory } from 'react-router-dom';
import { Projection } from '../../common/projections';
import { pickKeys } from '../../common/utils/pick_keys';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters';
import { useUrlParams } from './useUrlParams';
import {
LocalUIFilterName,
localUIFilters,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../server/lib/ui_filters/local_ui_filters/config';
import { history } from '../utils/history';
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
import { fromQuery, toQuery } from '../components/shared/Links/url_helpers';
import { removeUndefinedProps } from '../context/UrlParamsContext/helpers';
import { Projection } from '../../common/projections';
import { pickKeys } from '../../common/utils/pick_keys';
import { useCallApi } from './useCallApi';
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
const getInitialData = (
filterNames: LocalUIFilterName[]
@ -39,6 +39,7 @@ export function useLocalUIFilters({
filterNames: LocalUIFilterName[];
params?: Record<string, string | number | boolean | undefined>;
}) {
const history = useHistory();
const { uiFilters, urlParams } = useUrlParams();
const callApi = useCallApi();

View file

@ -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 { createHashHistory } from 'history';
// Make history singleton available across APM project
// TODO: Explore using React context or hook instead?
let history = createHashHistory();
export const resetHistory = () => {
history = createHashHistory();
};
export { history };

View file

@ -26,6 +26,20 @@ import {
} from '../../typings/elasticsearch';
import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext';
const originalConsoleWarn = console.warn; // eslint-disable-line no-console
/**
* A dependency we're using is using deprecated react methods. Override the
* console to hide the warnings. These should go away when we switch to
* Elastic Charts
*/
export function disableConsoleWarning(messageToDisable: string) {
return jest.spyOn(console, 'warn').mockImplementation((message) => {
if (!message.startsWith(messageToDisable)) {
originalConsoleWarn(message);
}
});
}
export function toJson(wrapper: ReactWrapper) {
return enzymeToJson(wrapper, {
noKey: true,

View file

@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createMemoryHistory } from 'history';
import React from 'react';
import { renderApp } from './';
import { Observable } from 'rxjs';
import { CoreStart, AppMountParameters } from 'src/core/public';
import { AppMountParameters, CoreStart } from 'src/core/public';
import { renderApp } from './';
describe('renderApp', () => {
it('renders', () => {
@ -19,6 +20,7 @@ describe('renderApp', () => {
} as unknown) as CoreStart;
const params = ({
element: window.document.createElement('div'),
history: createMemoryHistory(),
} as unknown) as AppMountParameters;
expect(() => {

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { createHashHistory } from 'history';
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
@ -52,10 +51,10 @@ function App() {
);
}
export const renderApp = (core: CoreStart, { element }: AppMountParameters) => {
export const renderApp = (core: CoreStart, { element, history }: AppMountParameters) => {
const i18nCore = core.i18n;
const isDarkMode = core.uiSettings.get('theme:darkMode');
const history = createHashHistory();
ReactDOM.render(
<PluginContext.Provider value={{ core }}>
<Router history={history}>