From 9b5912203f2211bd16f871a5f05cb59f1cae173a Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 31 Aug 2020 08:43:35 -0500 Subject: [PATCH] Use platform history (#74328) Co-authored-by: Elastic Machine Co-authored-by: cauemarcondes --- .../public/application/application.test.tsx | 74 +++++ .../plugins/apm/public/application/csmApp.tsx | 44 ++- .../plugins/apm/public/application/index.tsx | 113 ++++--- .../ErrorGroupDetails/DetailView/index.tsx | 17 +- .../List/__test__/List.test.tsx | 11 +- .../__test__/__snapshots__/List.test.tsx.snap | 48 +-- .../app/Home/__snapshots__/Home.test.tsx.snap | 34 +++ .../app/Main/UpdateBreadcrumbs.test.tsx | 133 ++++++--- .../components/app/Main/UpdateBreadcrumbs.tsx | 25 +- .../app/Main/route_config/index.tsx | 30 +- .../Main/route_config/route_config.test.tsx | 40 +++ .../route_handlers/agent_configuration.tsx | 6 +- .../RumDashboard/Charts/PageViewsChart.tsx | 19 +- .../app/ServiceMap/Controls.test.tsx | 45 +++ .../components/app/ServiceMap/Controls.tsx | 26 +- .../app/ServiceMap/Popover/Buttons.test.tsx | 14 +- .../app/ServiceMap/Popover/Buttons.tsx | 23 +- .../components/app/ServiceMap/index.test.tsx | 16 +- .../__test__/ServiceOverview.test.tsx | 56 ++-- .../ServiceOverview.test.tsx.snap | 4 +- .../SettingsPage/SettingsPage.tsx | 33 +- .../AgentConfigurationCreateEdit/index.tsx | 28 +- .../AgentConfigurations/List/index.tsx | 35 ++- .../Settings/AgentConfigurations/index.tsx | 19 +- .../components/app/Settings/Settings.test.tsx | 34 +++ .../public/components/app/Settings/index.tsx | 21 +- .../TransactionDetails/Distribution/index.tsx | 3 +- .../WaterfallWithSummmary/TransactionTabs.tsx | 3 +- .../Waterfall/WaterfallFlyout.tsx | 16 +- .../WaterfallContainer/Waterfall/index.tsx | 9 +- .../WaterfallWithSummmary/index.tsx | 3 +- .../TransactionOverview.test.tsx | 39 ++- .../app/TransactionOverview/index.tsx | 42 ++- .../app/TransactionOverview/useRedirect.ts | 9 +- .../DatePicker/__test__/DatePicker.test.tsx | 19 +- .../components/shared/DatePicker/index.tsx | 11 +- .../shared/EnvironmentFilter/index.tsx | 13 +- .../components/shared/KueryBar/index.tsx | 25 +- .../Links/DiscoverLinks/DiscoverLink.tsx | 10 +- .../components/shared/Links/InfraLink.tsx | 8 +- .../shared/Links/apm/APMLink.test.tsx | 82 ++--- .../components/shared/Links/apm/APMLink.tsx | 36 ++- .../Links/apm/agentConfigurationLinks.tsx | 36 ++- .../ServiceNameFilter/index.tsx | 34 ++- .../TransactionTypeFilter/index.tsx | 9 +- .../components/shared/ManagedTable/index.tsx | 7 +- .../components/shared/MetadataTable/index.tsx | 19 +- .../CustomLink/CustomLinkPopover.test.tsx | 22 +- .../CustomLink/ManageCustomLink.test.tsx | 16 +- .../CustomLink/index.test.tsx | 29 +- .../TransactionActionMenu.tsx | 39 +-- .../__test__/TransactionActionMenu.test.tsx | 282 ++++++++---------- .../TransactionActionMenu.test.tsx.snap | 2 +- .../__test__/sections.test.ts | 6 +- .../shared/TransactionActionMenu/sections.ts | 6 +- .../charts/CustomPlot/test/CustomPlot.test.js | 15 +- .../Histogram/__test__/Histogram.test.js | 16 +- .../Timeline/Marker/ErrorMarker.test.tsx | 24 +- .../shared/charts/Timeline/Timeline.test.tsx | 8 + .../ApmPluginContext/MockApmPluginContext.tsx | 18 +- .../public/context/ApmPluginContext/index.tsx | 8 +- .../apm/public/context/ChartsSyncContext.tsx | 9 +- .../apm/public/hooks/useLocalUIFilters.ts | 13 +- x-pack/plugins/apm/public/utils/history.ts | 17 -- .../plugins/apm/public/utils/testHelpers.tsx | 14 + .../public/application/application.test.tsx | 6 +- .../public/application/index.tsx | 5 +- 67 files changed, 1190 insertions(+), 746 deletions(-) create mode 100644 x-pack/plugins/apm/public/application/application.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx delete mode 100644 x-pack/plugins/apm/public/utils/history.ts diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx new file mode 100644 index 0000000000000..fc369b9cf672a --- /dev/null +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -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(); + }); +}); diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index cf3fe2decfa44..d76ed5c2100b2 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -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 ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); } @@ -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( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 5e502f58e5f56..3f4f3116152c4 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -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 { ApmPluginSetupDeps } from '../plugin'; +import { createCallApmApi } from '../services/rest/createCallApmApi'; 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 { px, units } from '../style/variables'; import { setReadonlyBadge } from '../updateBadge'; -import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { ConfigSchema } from '..'; -import 'react-vis/dist/style.css'; 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 ( - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ); } @@ -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( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 4e1af6e0dc239..5202ca13ed102 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -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 { first } from 'lodash'; import React from 'react'; +import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; -import { first } from 'lodash'; // 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) { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx index a173f4068db6a..5798deaf19c9c 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx @@ -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( - - - + + + + + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 40522edc21b52..5183432b4ae0f 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -784,11 +784,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > a0ce2 @@ -829,11 +829,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -876,13 +876,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > f3ac9 @@ -1063,11 +1063,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1110,13 +1110,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > e9086 @@ -1297,11 +1297,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1344,13 +1344,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > 8673d @@ -1531,11 +1531,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1578,13 +1578,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > { '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', diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx index e7657c63f41bb..5bf5cea587f93 100644 --- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx @@ -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 { 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) => { + if (href) { + event.preventDefault(); + this.props.core.application.navigateToUrl(href); + } + }, }; } ); diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 8caddc94b6907..56026dcf477ec 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -38,14 +38,28 @@ interface RouteParams { } export const renderAsRedirectTo = (to: string) => { - return ({ location }: RouteComponentProps) => ( - - ); + return ({ location }: RouteComponentProps) => { + 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 ( + + ); + }; }; export const routes: BreadcrumbRoute[] = [ diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx new file mode 100644 index 0000000000000..ad12afe35fa20 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx @@ -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', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx index 14c912d0bd519..d99dc4d5cd37a 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx @@ -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 diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx index 9211504a2dffe..c76be19edfe47 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx @@ -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>; @@ -38,6 +38,7 @@ interface Props { } export function PageViewsChart({ data, loading }: Props) { + const history = useHistory(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx new file mode 100644 index 0000000000000..1187b71dff825 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx @@ -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 ( + + + + {children} + + + + ); +} + +describe('Controls', () => { + describe('with a primary node', () => { + it('links to the full map', async () => { + const result = render(, { wrapper: Wrapper }); + const { findByTestId } = result; + + const button = await findByTestId('viewFullMapButton'); + + expect(button.getAttribute('href')).toEqual( + '/basepath/app/apm/service-map' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index bcc87cbf35819..c8f586240471f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -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 { CytoscapeContext } from './Cytoscape'; -import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions'; -import { getAPMHref } from '../../shared/Links/apm/APMLink'; +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 { useTheme } from '../../../hooks/useTheme'; +import { CytoscapeContext } from './Cytoscape'; +import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions'; 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() {