[APM] Add Obs side nav and refactor APM templates (#101044)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Søren Louv-Jansen 2021-06-04 02:59:30 +02:00 committed by GitHub
parent be9fcad655
commit caa4bd111d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
103 changed files with 1637 additions and 2052 deletions

View file

@ -37,6 +37,18 @@ export function getEnvironmentLabel(environment: string) {
return environmentLabels[environment] || environment;
}
export function omitEsFieldValue({
esFieldValue,
value,
text,
}: {
esFieldValue?: string;
value: string;
text: string;
}) {
return { value, text };
}
export function parseEnvironmentUrlParam(environment: string) {
if (environment === ENVIRONMENT_ALL_VALUE) {
return ENVIRONMENT_ALL;

View file

@ -51,16 +51,16 @@ Then(`should display percentile for page load chart`, () => {
cy.get(pMarkers).eq(3).should('have.text', '95th');
});
Then(`should display chart legend`, () => {
const chartLegend = 'button.echLegendItem__label';
// Then(`should display chart legend`, () => {
// const chartLegend = 'button.echLegendItem__label';
waitForLoadingToFinish();
cy.get('.euiLoadingChart').should('not.exist');
// waitForLoadingToFinish();
// cy.get('.euiLoadingChart').should('not.exist');
cy.get('[data-cy=pageLoadDist]').within(() => {
cy.get(chartLegend, DEFAULT_TIMEOUT).eq(0).should('have.text', 'Overall');
});
});
// cy.get('[data-cy=pageLoadDist]').within(() => {
// cy.get(chartLegend, DEFAULT_TIMEOUT).eq(0).should('have.text', 'Overall');
// });
// });
Then(`should display tooltip on hover`, () => {
cy.get('.euiLoadingChart').should('not.exist');

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import React from 'react';
import { act } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Observable } from 'rxjs';
@ -15,6 +16,7 @@ import { renderApp } from './';
import { disableConsoleWarning } from '../utils/testHelpers';
import { dataPluginMock } from 'src/plugins/data/public/mocks';
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
import { ApmPluginStartDeps } from '../plugin';
jest.mock('../services/rest/index_pattern', () => ({
createStaticIndexPattern: () => Promise.resolve(undefined),
@ -44,6 +46,7 @@ describe('renderApp', () => {
config,
observabilityRuleTypeRegistry,
} = mockApmPluginContextValue;
const plugins = {
licensing: { license$: new Observable() },
triggersActionsUi: { actionTypeRegistry: {}, alertTypeRegistry: {} },
@ -56,7 +59,7 @@ describe('renderApp', () => {
},
},
};
const params = {
const appMountParameters = {
element: document.createElement('div'),
history: createMemoryHistory(),
setHeaderActionMenu: () => {},
@ -64,7 +67,16 @@ describe('renderApp', () => {
const data = dataPluginMock.createStartContract();
const embeddable = embeddablePluginMock.createStartContract();
const startDeps = {
const pluginsStart = ({
observability: {
navigation: {
registerSections: () => jest.fn(),
PageTemplate: ({ children }: { children: React.ReactNode }) => (
<div>hello worlds {children}</div>
),
},
},
triggersActionsUi: {
actionTypeRegistry: {},
alertTypeRegistry: {},
@ -73,7 +85,8 @@ describe('renderApp', () => {
},
data,
embeddable,
};
} as unknown) as ApmPluginStartDeps;
jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined);
createCallApmApi((core as unknown) as CoreStart);
@ -93,8 +106,8 @@ describe('renderApp', () => {
unmount = renderApp({
coreStart: core as any,
pluginsSetup: plugins as any,
appMountParameters: params as any,
pluginsStart: startDeps as any,
appMountParameters: appMountParameters as any,
pluginsStart,
config,
observabilityRuleTypeRegistry,
});

View file

@ -20,7 +20,6 @@ import {
useUiSetting$,
} from '../../../../../src/plugins/kibana_react/public';
import { APMRouteDefinition } from '../application/routes';
import { renderAsRedirectTo } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { RumHome, UX_LABEL } from '../components/app/RumDashboard/RumHome';
import { ApmPluginContext } from '../context/apm_plugin/apm_plugin_context';
@ -32,6 +31,7 @@ import { createCallApmApi } from '../services/rest/createCallApmApi';
import { px, units } from '../style/variables';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import { UXActionMenu } from '../components/app/RumDashboard/ActionMenu';
import { redirectTo } from '../components/routing/redirect_to';
const CsmMainContainer = euiStyled.div`
padding: ${px(units.plus)};
@ -42,7 +42,7 @@ export const rumRoutes: APMRouteDefinition[] = [
{
exact: true,
path: '/',
render: renderAsRedirectTo('/ux'),
render: redirectTo('/ux'),
breadcrumb: UX_LABEL,
},
];

View file

@ -5,99 +5,18 @@
* 2.0.
*/
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 'react-vis/dist/style.css';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import type { ObservabilityRuleTypeRegistry } from '../../../observability/public';
import { euiStyled } from '../../../../../src/plugins/kibana_react/common';
import { ConfigSchema } from '../';
import { AppMountParameters, CoreStart } from '../../../../../src/core/public';
import {
KibanaContextProvider,
RedirectAppLinks,
useUiSetting$,
} from '../../../../../src/plugins/kibana_react/public';
import { routes } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import {
ApmPluginContext,
ApmPluginContextValue,
} from '../context/apm_plugin/apm_plugin_context';
import { LicenseProvider } from '../context/license/license_context';
import { UrlParamsProvider } from '../context/url_params_context/url_params_context';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/createCallApmApi';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import { setHelpExtension } from '../setHelpExtension';
import { setReadonlyBadge } from '../updateBadge';
import { AnomalyDetectionJobsContextProvider } from '../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
const MainContainer = euiStyled.div`
height: 100%;
`;
function App() {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
useBreadcrumbs(routes);
return (
<ThemeProvider
theme={(outerTheme?: DefaultTheme) => ({
...outerTheme,
eui: darkMode ? euiDarkVars : euiLightVars,
darkMode,
})}
>
<MainContainer data-test-subj="apmMainContainer" role="main">
<Route component={ScrollToTopOnPathChange} />
<Switch>
{routes.map((route, i) => (
<ApmRoute key={i} {...route} />
))}
</Switch>
</MainContainer>
</ThemeProvider>
);
}
export function ApmAppRoot({
apmPluginContextValue,
startDeps,
}: {
apmPluginContextValue: ApmPluginContextValue;
startDeps: ApmPluginStartDeps;
}) {
const { appMountParameters, core } = apmPluginContextValue;
const { history } = appMountParameters;
const i18nCore = core.i18n;
return (
<RedirectAppLinks application={core.application}>
<ApmPluginContext.Provider value={apmPluginContextValue}>
<KibanaContextProvider services={{ ...core, ...startDeps }}>
<i18nCore.Context>
<Router history={history}>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<App />
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</ApmPluginContext.Provider>
</RedirectAppLinks>
);
}
import { ApmAppRoot } from '../components/routing/app_root';
/**
* This module is rendered asynchronously in the Kibana platform.
@ -141,7 +60,7 @@ export const renderApp = ({
ReactDOM.render(
<ApmAppRoot
apmPluginContextValue={apmPluginContextValue}
startDeps={pluginsStart}
pluginsStart={pluginsStart}
/>,
element
);

View file

@ -10,24 +10,17 @@ import { useParams } from 'react-router-dom';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { AlertType } from '../../../../common/alert_types';
import { getInitialAlertValues } from '../get_initial_alert_values';
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
import { ApmPluginStartDeps } from '../../../plugin';
interface Props {
addFlyoutVisible: boolean;
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
alertType: AlertType | null;
}
interface KibanaDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export function AlertingFlyout(props: Props) {
const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props;
const { serviceName } = useParams<{ serviceName?: string }>();
const {
services: { triggersActionsUi },
} = useKibana<KibanaDeps>();
const { services } = useKibana<ApmPluginStartDeps>();
const initialValues = getInitialAlertValues(alertType, serviceName);
const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [
@ -37,7 +30,7 @@ export function AlertingFlyout(props: Props) {
const addAlertFlyout = useMemo(
() =>
alertType &&
triggersActionsUi.getAddAlertFlyout({
services.triggersActionsUi.getAddAlertFlyout({
consumer: 'apm',
onClose: onCloseAddFlyout,
alertTypeId: alertType,
@ -45,7 +38,7 @@ export function AlertingFlyout(props: Props) {
initialValues,
}),
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[alertType, onCloseAddFlyout, triggersActionsUi]
[alertType, onCloseAddFlyout, services.triggersActionsUi]
);
return <>{addFlyoutVisible && addAlertFlyout}</>;
}

View file

@ -6,7 +6,6 @@
*/
import { shallow } from 'enzyme';
import { Location } from 'history';
import React from 'react';
import { mockMoment } from '../../../../utils/testHelpers';
import { DetailView } from './index';
@ -19,11 +18,7 @@ describe('DetailView', () => {
it('should render empty state', () => {
const wrapper = shallow(
<DetailView
errorGroup={{} as any}
urlParams={{}}
location={{} as Location}
/>
<DetailView errorGroup={{} as any} urlParams={{}} />
);
expect(wrapper.isEmptyRender()).toBe(true);
});
@ -46,11 +41,7 @@ describe('DetailView', () => {
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{} as Location}
/>
<DetailView errorGroup={errorGroup} urlParams={{}} />
).find('DiscoverErrorLink');
expect(wrapper.exists()).toBe(true);
@ -69,11 +60,7 @@ describe('DetailView', () => {
transaction: undefined,
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{} as Location}
/>
<DetailView errorGroup={errorGroup} urlParams={{}} />
).find('Summary');
expect(wrapper.exists()).toBe(true);
@ -93,11 +80,7 @@ describe('DetailView', () => {
} as any,
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{} as Location}
/>
<DetailView errorGroup={errorGroup} urlParams={{}} />
).find('EuiTabs');
expect(wrapper.exists()).toBe(true);
@ -117,11 +100,7 @@ describe('DetailView', () => {
} as any,
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{} as Location}
/>
<DetailView errorGroup={errorGroup} urlParams={{}} />
).find('TabContent');
expect(wrapper.exists()).toBe(true);
@ -145,13 +124,7 @@ describe('DetailView', () => {
} as any,
};
expect(() =>
shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{} as Location}
/>
)
shallow(<DetailView errorGroup={errorGroup} urlParams={{}} />)
).not.toThrowError();
});
});

View file

@ -16,7 +16,6 @@ import {
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';
@ -58,7 +57,6 @@ const TransactionLinkName = euiStyled.div`
interface Props {
errorGroup: APIReturnType<'GET /api/apm/services/{serviceName}/errors/{groupId}'>;
urlParams: IUrlParams;
location: Location;
}
// TODO: Move query-string-based tabs into a re-usable component?
@ -70,7 +68,7 @@ function getCurrentTab(
return selectedTab ? selectedTab : first(tabs) || {};
}
export function DetailView({ errorGroup, urlParams, location }: Props) {
export function DetailView({ errorGroup, urlParams }: Props) {
const history = useHistory();
const { transaction, error, occurrencesCount } = errorGroup;

View file

@ -9,24 +9,19 @@ import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import React from 'react';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { useTrackPageview } from '../../../../../observability/public';
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
import { ApmHeader } from '../../shared/ApmHeader';
import { SearchBar } from '../../shared/search_bar';
import { DetailView } from './DetailView';
import { ErrorDistribution } from './Distribution';
import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
@ -68,44 +63,42 @@ function ErrorGroupHeader({
isUnhandled?: boolean;
}) {
return (
<>
<ApmHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle>
<h1>
{i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', {
defaultMessage: 'Error group {errorGroupId}',
values: {
errorGroupId: getShortGroupId(groupId),
},
})}
</h1>
</EuiTitle>
</EuiFlexItem>
{isUnhandled && (
<EuiFlexItem grow={false}>
<EuiBadge color="warning">
{i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', {
defaultMessage: 'Unhandled',
})}
</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
</ApmHeader>
<SearchBar />
</>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle>
<h2>
{i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', {
defaultMessage: 'Error group {errorGroupId}',
values: {
errorGroupId: getShortGroupId(groupId),
},
})}
</h2>
</EuiTitle>
</EuiFlexItem>
{isUnhandled && (
<EuiFlexItem grow={false}>
<EuiBadge color="warning">
{i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', {
defaultMessage: 'Unhandled',
})}
</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}
type ErrorGroupDetailsProps = RouteComponentProps<{
interface ErrorGroupDetailsProps {
groupId: string;
serviceName: string;
}>;
}
export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) {
const { serviceName, groupId } = match.params;
export function ErrorGroupDetails({
serviceName,
groupId,
}: ErrorGroupDetailsProps) {
const { urlParams } = useUrlParams();
const { environment, kuery, start, end } = urlParams;
const { data: errorGroupData } = useFetcher(
@ -154,66 +147,56 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) {
return (
<>
<ErrorGroupHeader groupId={groupId} isUnhandled={isUnhandled} />
<EuiPage>
<EuiPageBody>
<EuiPanel>
{showDetails && (
<Titles>
<EuiText>
{logMessage && (
<Fragment>
<Label>
{i18n.translate(
'xpack.apm.errorGroupDetails.logMessageLabel',
{
defaultMessage: 'Log message',
}
)}
</Label>
<Message>{logMessage}</Message>
</Fragment>
)}
<EuiPanel>
{showDetails && (
<Titles>
<EuiText>
{logMessage && (
<>
<Label>
{i18n.translate(
'xpack.apm.errorGroupDetails.exceptionMessageLabel',
'xpack.apm.errorGroupDetails.logMessageLabel',
{
defaultMessage: 'Exception message',
defaultMessage: 'Log message',
}
)}
</Label>
<Message>{excMessage || NOT_AVAILABLE_LABEL}</Message>
<Label>
{i18n.translate(
'xpack.apm.errorGroupDetails.culpritLabel',
{
defaultMessage: 'Culprit',
}
)}
</Label>
<Culprit>{culprit || NOT_AVAILABLE_LABEL}</Culprit>
</EuiText>
</Titles>
)}
<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
{
defaultMessage: 'Occurrences',
}
<Message>{logMessage}</Message>
</>
)}
/>
</EuiPanel>
<EuiSpacer size="s" />
{showDetails && (
<DetailView
errorGroup={errorGroupData}
urlParams={urlParams}
location={location}
/>
<Label>
{i18n.translate(
'xpack.apm.errorGroupDetails.exceptionMessageLabel',
{
defaultMessage: 'Exception message',
}
)}
</Label>
<Message>{excMessage || NOT_AVAILABLE_LABEL}</Message>
<Label>
{i18n.translate('xpack.apm.errorGroupDetails.culpritLabel', {
defaultMessage: 'Culprit',
})}
</Label>
<Culprit>{culprit || NOT_AVAILABLE_LABEL}</Culprit>
</EuiText>
</Titles>
)}
<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
{
defaultMessage: 'Occurrences',
}
)}
</EuiPageBody>
</EuiPage>
/>
</EuiPanel>
<EuiSpacer size="s" />
{showDetails && (
<DetailView errorGroup={errorGroupData} urlParams={urlParams} />
)}
</>
);
}

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { Home } from '../Home';
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
describe('Home component', () => {
it('should render services', () => {
expect(
shallow(
<MockApmPluginContextWrapper>
<Home tab="services" />
</MockApmPluginContextWrapper>
)
).toMatchSnapshot();
});
it('should render traces', () => {
expect(
shallow(
<MockApmPluginContextWrapper>
<Home tab="traces" />
</MockApmPluginContextWrapper>
)
).toMatchSnapshot();
});
});

View file

@ -1,189 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Home component should render services 1`] = `
<ContextProvider
value={
Object {
"appMountParameters": Object {
"setHeaderActionMenu": [Function],
},
"config": Object {
"profilingEnabled": false,
"serviceMapEnabled": true,
"ui": Object {
"enabled": false,
},
},
"core": Object {
"application": Object {
"capabilities": Object {
"apm": Object {},
"ml": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
},
"navigateToUrl": [Function],
},
"chrome": Object {
"docTitle": Object {
"change": [Function],
},
"setBadge": [Function],
"setBreadcrumbs": [Function],
"setHelpExtension": [Function],
},
"docLinks": Object {
"DOC_LINK_VERSION": "0",
"ELASTIC_WEBSITE_URL": "https://www.elastic.co/",
},
"http": Object {
"basePath": Object {
"get": [Function],
"prepend": [Function],
},
},
"i18n": Object {
"Context": [Function],
},
"notifications": Object {
"toasts": Object {
"addDanger": [Function],
"addWarning": [Function],
},
},
"uiSettings": Object {
"get": [Function],
"get$": [Function],
},
},
"observabilityRuleTypeRegistry": Object {
"registerFormatter": [Function],
},
"plugins": Object {
"data": Object {
"query": Object {
"timefilter": Object {
"timefilter": Object {
"getTime": [Function],
"setTime": [Function],
},
},
},
},
"ml": Object {
"urlGenerator": MlUrlGenerator {
"createUrl": [Function],
"id": "ML_APP_URL_GENERATOR",
"params": Object {
"appBasePath": "/app/ml",
"useHash": false,
},
},
},
"observability": Object {
"isAlertingExperienceEnabled": [Function],
},
},
}
}
>
<Home
tab="services"
/>
</ContextProvider>
`;
exports[`Home component should render traces 1`] = `
<ContextProvider
value={
Object {
"appMountParameters": Object {
"setHeaderActionMenu": [Function],
},
"config": Object {
"profilingEnabled": false,
"serviceMapEnabled": true,
"ui": Object {
"enabled": false,
},
},
"core": Object {
"application": Object {
"capabilities": Object {
"apm": Object {},
"ml": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
},
"navigateToUrl": [Function],
},
"chrome": Object {
"docTitle": Object {
"change": [Function],
},
"setBadge": [Function],
"setBreadcrumbs": [Function],
"setHelpExtension": [Function],
},
"docLinks": Object {
"DOC_LINK_VERSION": "0",
"ELASTIC_WEBSITE_URL": "https://www.elastic.co/",
},
"http": Object {
"basePath": Object {
"get": [Function],
"prepend": [Function],
},
},
"i18n": Object {
"Context": [Function],
},
"notifications": Object {
"toasts": Object {
"addDanger": [Function],
"addWarning": [Function],
},
},
"uiSettings": Object {
"get": [Function],
"get$": [Function],
},
},
"observabilityRuleTypeRegistry": Object {
"registerFormatter": [Function],
},
"plugins": Object {
"data": Object {
"query": Object {
"timefilter": Object {
"timefilter": Object {
"getTime": [Function],
"setTime": [Function],
},
},
},
},
"ml": Object {
"urlGenerator": MlUrlGenerator {
"createUrl": [Function],
"id": "ML_APP_URL_GENERATOR",
"params": Object {
"appBasePath": "/app/ml",
"useHash": false,
},
},
},
"observability": Object {
"isAlertingExperienceEnabled": [Function],
},
},
}
}
>
<Home
tab="traces"
/>
</ContextProvider>
`;

View file

@ -1,80 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiTab, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { ComponentType } from 'react';
import { $ElementType } from 'utility-types';
import { ApmHeader } from '../../shared/ApmHeader';
import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink';
import { useServiceInventoryHref } from '../../shared/Links/apm/service_inventory_link';
import { useTraceOverviewHref } from '../../shared/Links/apm/TraceOverviewLink';
import { MainTabs } from '../../shared/main_tabs';
import { ServiceMap } from '../ServiceMap';
import { ServiceInventory } from '../service_inventory';
import { TraceOverview } from '../trace_overview';
interface Tab {
key: string;
href: string;
text: string;
Component: ComponentType;
}
interface Props {
tab: 'traces' | 'services' | 'service-map';
}
export function Home({ tab }: Props) {
const homeTabs: Tab[] = [
{
key: 'services',
href: useServiceInventoryHref(),
text: i18n.translate('xpack.apm.home.servicesTabLabel', {
defaultMessage: 'Services',
}),
Component: ServiceInventory,
},
{
key: 'traces',
href: useTraceOverviewHref(),
text: i18n.translate('xpack.apm.home.tracesTabLabel', {
defaultMessage: 'Traces',
}),
Component: TraceOverview,
},
{
key: 'service-map',
href: useServiceMapHref(),
text: i18n.translate('xpack.apm.home.serviceMapTabLabel', {
defaultMessage: 'Service Map',
}),
Component: ServiceMap,
},
];
const selectedTab = homeTabs.find(
(homeTab) => homeTab.key === tab
) as $ElementType<typeof homeTabs, number>;
return (
<>
<ApmHeader>
<EuiTitle>
<h1>APM</h1>
</EuiTitle>
</ApmHeader>
<MainTabs>
{homeTabs.map(({ href, key, text }) => (
<EuiTab href={href} isSelected={key === selectedTab.key} key={key}>
{text}
</EuiTab>
))}
</MainTabs>
<selectedTab.Component />
</>
);
}

View file

@ -1,369 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context';
import { getServiceNodeName } from '../../../../../common/service_nodes';
import { APMRouteDefinition } from '../../../../application/routes';
import { toQuery } from '../../../shared/Links/url_helpers';
import { ErrorGroupDetails } from '../../ErrorGroupDetails';
import { Home } from '../../Home';
import { ServiceDetails } from '../../service_details';
import { ServiceNodeMetrics } from '../../service_node_metrics';
import { Settings } from '../../Settings';
import { AgentConfigurations } from '../../Settings/AgentConfigurations';
import { AnomalyDetection } from '../../Settings/anomaly_detection';
import { ApmIndices } from '../../Settings/ApmIndices';
import { CustomizeUI } from '../../Settings/CustomizeUI';
import { TraceLink } from '../../TraceLink';
import { TransactionDetails } from '../../transaction_details';
import {
CreateAgentConfigurationRouteHandler,
EditAgentConfigurationRouteHandler,
} from './route_handlers/agent_configuration';
import { enableServiceOverview } from '../../../../../common/ui_settings_keys';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
/**
* Given a path, redirect to that location, preserving the search and maintaining
* backward-compatibilty with legacy (pre-7.9) hash-based URLs.
*/
export function renderAsRedirectTo(to: string) {
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 (
<Redirect
to={{
...location,
hash: '',
pathname: to,
search: resolvedUrl ? resolvedUrl.search : location.search,
}}
/>
);
};
}
// These component function definitions are used below with the `component`
// property of the route definitions.
//
// If you provide an inline function to the component prop, you would create a
// new component every render. This results in the existing component unmounting
// and the new component mounting instead of just updating the existing component.
function HomeServices() {
return <Home tab="services" />;
}
function HomeServiceMap() {
return <Home tab="service-map" />;
}
function HomeTraces() {
return <Home tab="traces" />;
}
function ServiceDetailsErrors(
props: RouteComponentProps<{ serviceName: string }>
) {
return <ServiceDetails {...props} tab="errors" />;
}
function ServiceDetailsMetrics(
props: RouteComponentProps<{ serviceName: string }>
) {
return <ServiceDetails {...props} tab="metrics" />;
}
function ServiceDetailsNodes(
props: RouteComponentProps<{ serviceName: string }>
) {
return <ServiceDetails {...props} tab="nodes" />;
}
function ServiceDetailsOverview(
props: RouteComponentProps<{ serviceName: string }>
) {
return <ServiceDetails {...props} tab="overview" />;
}
function ServiceDetailsServiceMap(
props: RouteComponentProps<{ serviceName: string }>
) {
return <ServiceDetails {...props} tab="service-map" />;
}
function ServiceDetailsTransactions(
props: RouteComponentProps<{ serviceName: string }>
) {
return <ServiceDetails {...props} tab="transactions" />;
}
function ServiceDetailsProfiling(
props: RouteComponentProps<{ serviceName: string }>
) {
return <ServiceDetails {...props} tab="profiling" />;
}
function SettingsAgentConfiguration(props: RouteComponentProps<{}>) {
return (
<Settings {...props}>
<AgentConfigurations />
</Settings>
);
}
function SettingsAnomalyDetection(props: RouteComponentProps<{}>) {
return (
<Settings {...props}>
<AnomalyDetection />
</Settings>
);
}
function SettingsApmIndices(props: RouteComponentProps<{}>) {
return (
<Settings {...props}>
<ApmIndices />
</Settings>
);
}
function SettingsCustomizeUI(props: RouteComponentProps<{}>) {
return (
<Settings {...props}>
<CustomizeUI />
</Settings>
);
}
function DefaultServicePageRouteHandler(
props: RouteComponentProps<{ serviceName: string }>
) {
const { uiSettings } = useApmPluginContext().core;
const { serviceName } = props.match.params;
if (uiSettings.get(enableServiceOverview)) {
return renderAsRedirectTo(`/services/${serviceName}/overview`)(props);
}
return renderAsRedirectTo(`/services/${serviceName}/transactions`)(props);
}
/**
* The array of route definitions to be used when the application
* creates the routes.
*/
export const routes: APMRouteDefinition[] = [
{
exact: true,
path: '/',
render: renderAsRedirectTo('/services'),
breadcrumb: 'APM',
},
// !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
{
exact: true,
path: '/services',
component: HomeServices,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', {
defaultMessage: 'Services',
}),
},
// !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
{
exact: true,
path: '/traces',
component: HomeTraces,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', {
defaultMessage: 'Traces',
}),
},
{
exact: true,
path: '/settings',
render: renderAsRedirectTo('/settings/agent-configuration'),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', {
defaultMessage: 'Settings',
}),
},
{
exact: true,
path: '/settings/apm-indices',
component: SettingsApmIndices,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', {
defaultMessage: 'Indices',
}),
},
{
exact: true,
path: '/settings/agent-configuration',
component: SettingsAgentConfiguration,
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.agentConfigurationTitle',
{ defaultMessage: 'Agent Configuration' }
),
},
{
exact: true,
path: '/settings/agent-configuration/create',
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.createAgentConfigurationTitle',
{ defaultMessage: 'Create Agent Configuration' }
),
component: CreateAgentConfigurationRouteHandler,
},
{
exact: true,
path: '/settings/agent-configuration/edit',
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.editAgentConfigurationTitle',
{ defaultMessage: 'Edit Agent Configuration' }
),
component: EditAgentConfigurationRouteHandler,
},
{
exact: true,
path: '/services/:serviceName',
breadcrumb: ({ match }) => match.params.serviceName,
component: DefaultServicePageRouteHandler,
} as APMRouteDefinition<{ serviceName: string }>,
{
exact: true,
path: '/services/:serviceName/overview',
breadcrumb: i18n.translate('xpack.apm.breadcrumb.overviewTitle', {
defaultMessage: 'Overview',
}),
component: withApmServiceContext(ServiceDetailsOverview),
} as APMRouteDefinition<{ serviceName: string }>,
// errors
{
exact: true,
path: '/services/:serviceName/errors/:groupId',
component: withApmServiceContext(ErrorGroupDetails),
breadcrumb: ({ match }) => match.params.groupId,
} as APMRouteDefinition<{ groupId: string; serviceName: string }>,
{
exact: true,
path: '/services/:serviceName/errors',
component: withApmServiceContext(ServiceDetailsErrors),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', {
defaultMessage: 'Errors',
}),
},
// transactions
{
exact: true,
path: '/services/:serviceName/transactions',
component: withApmServiceContext(ServiceDetailsTransactions),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', {
defaultMessage: 'Transactions',
}),
},
// metrics
{
exact: true,
path: '/services/:serviceName/metrics',
component: withApmServiceContext(ServiceDetailsMetrics),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
defaultMessage: 'Metrics',
}),
},
// service nodes, only enabled for java agents for now
{
exact: true,
path: '/services/:serviceName/nodes',
component: withApmServiceContext(ServiceDetailsNodes),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', {
defaultMessage: 'JVMs',
}),
},
// node metrics
{
exact: true,
path: '/services/:serviceName/nodes/:serviceNodeName/metrics',
component: withApmServiceContext(ServiceNodeMetrics),
breadcrumb: ({ match }) => getServiceNodeName(match.params.serviceNodeName),
},
{
exact: true,
path: '/services/:serviceName/transactions/view',
component: withApmServiceContext(TransactionDetails),
breadcrumb: ({ location }) => {
const query = toQuery(location.search);
return query.transactionName as string;
},
},
{
exact: true,
path: '/services/:serviceName/profiling',
component: withApmServiceContext(ServiceDetailsProfiling),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceProfilingTitle', {
defaultMessage: 'Profiling',
}),
},
{
exact: true,
path: '/services/:serviceName/service-map',
component: withApmServiceContext(ServiceDetailsServiceMap),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
defaultMessage: 'Service Map',
}),
},
{
exact: true,
path: '/link-to/trace/:traceId',
component: TraceLink,
breadcrumb: null,
},
// !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
{
exact: true,
path: '/service-map',
component: HomeServiceMap,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
defaultMessage: 'Service Map',
}),
},
{
exact: true,
path: '/settings/customize-ui',
component: SettingsCustomizeUI,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', {
defaultMessage: 'Customize UI',
}),
},
{
exact: true,
path: '/settings/anomaly-detection',
component: SettingsAnomalyDetection,
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.anomalyDetection',
{
defaultMessage: 'Anomaly detection',
}
),
},
];
function withApmServiceContext(WrappedComponent: React.ComponentType<any>) {
return (props: any) => {
return (
<ApmServiceContextProvider>
<WrappedComponent {...props} />
</ApmServiceContextProvider>
);
};
}

View file

@ -42,12 +42,12 @@ export function AgentConfigurations() {
return (
<>
<EuiTitle size="l">
<h1>
<EuiTitle>
<h2>
{i18n.translate('xpack.apm.agentConfig.titleText', {
defaultMessage: 'Agent central configuration',
})}
</h1>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued">

View file

@ -25,11 +25,11 @@ describe('ApmIndices', () => {
);
expect(getByText('Indices')).toMatchInlineSnapshot(`
<h1
class="euiTitle euiTitle--large"
<h2
class="euiTitle euiTitle--medium"
>
Indices
</h1>
</h2>
`);
expect(spy).toHaveBeenCalledTimes(2);

View file

@ -176,12 +176,12 @@ export function ApmIndices() {
return (
<>
<EuiTitle size="l">
<h1>
<EuiTitle>
<h2>
{i18n.translate('xpack.apm.settings.apmIndices.title', {
defaultMessage: 'Indices',
})}
</h1>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued">

View file

@ -13,12 +13,12 @@ import { CustomLinkOverview } from './CustomLink';
export function CustomizeUI() {
return (
<>
<EuiTitle size="l">
<h1>
<EuiTitle>
<h2>
{i18n.translate('xpack.apm.settings.customizeApp.title', {
defaultMessage: 'Customize app',
})}
</h1>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued">

View file

@ -66,12 +66,12 @@ export function AnomalyDetection() {
return (
<>
<EuiTitle size="l">
<h1>
<EuiTitle>
<h2>
{i18n.translate('xpack.apm.settings.anomalyDetection.titleText', {
defaultMessage: 'Anomaly detection',
})}
</h1>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued">

View file

@ -5,32 +5,19 @@
* 2.0.
*/
import {
EuiButtonEmpty,
EuiPage,
EuiPageBody,
EuiPageSideBar,
EuiSideNav,
EuiSpacer,
} from '@elastic/eui';
import { EuiPage, EuiPageBody, EuiPageSideBar, EuiSideNav } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { ReactNode, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { HeaderMenuPortal } from '../../../../../observability/public';
import { ActionMenu } from '../../../application/action_menu';
import { useHistory } from 'react-router-dom';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { getAPMHref } from '../../shared/Links/apm/APMLink';
import { HomeLink } from '../../shared/Links/apm/HomeLink';
interface SettingsProps extends RouteComponentProps<{}> {
children: ReactNode;
}
export function Settings({ children, location }: SettingsProps) {
const { appMountParameters, core } = useApmPluginContext();
export function Settings({ children }: { children: ReactNode }) {
const { core } = useApmPluginContext();
const history = useHistory();
const { basePath } = core.http;
const canAccessML = !!core.application.capabilities.ml?.canAccessML;
const { search, pathname } = location;
const { search, pathname } = history.location;
const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false);
@ -43,86 +30,65 @@ export function Settings({ children, location }: SettingsProps) {
}
return (
<>
<HeaderMenuPortal
setHeaderActionMenu={appMountParameters.setHeaderActionMenu}
>
<ActionMenu />
</HeaderMenuPortal>
<EuiPage>
<EuiPageSideBar>
<HomeLink>
<EuiButtonEmpty
flush="left"
size="s"
color="primary"
iconType="arrowLeft"
>
{i18n.translate('xpack.apm.settings.returnLinkLabel', {
defaultMessage: 'Return to inventory',
})}
</EuiButtonEmpty>
<EuiSpacer size="s" />
</HomeLink>
<EuiSideNav
toggleOpenOnMobile={() => toggleOpenOnMobile()}
isOpenOnMobile={isSideNavOpenOnMobile}
items={[
{
name: i18n.translate('xpack.apm.settings.pageTitle', {
defaultMessage: 'Settings',
}),
id: 0,
items: [
{
name: i18n.translate('xpack.apm.settings.agentConfig', {
defaultMessage: 'Agent Configuration',
}),
id: '1',
href: getSettingsHref('/agent-configuration'),
isSelected: pathname.startsWith(
'/settings/agent-configuration'
),
},
...(canAccessML
? [
{
name: i18n.translate(
'xpack.apm.settings.anomalyDetection',
{
defaultMessage: 'Anomaly detection',
}
),
id: '4',
href: getSettingsHref('/anomaly-detection'),
isSelected:
pathname === '/settings/anomaly-detection',
},
]
: []),
{
name: i18n.translate('xpack.apm.settings.customizeApp', {
defaultMessage: 'Customize app',
}),
id: '3',
href: getSettingsHref('/customize-ui'),
isSelected: pathname === '/settings/customize-ui',
},
{
name: i18n.translate('xpack.apm.settings.indices', {
defaultMessage: 'Indices',
}),
id: '2',
href: getSettingsHref('/apm-indices'),
isSelected: pathname === '/settings/apm-indices',
},
],
},
]}
/>
</EuiPageSideBar>
<EuiPageBody>{children}</EuiPageBody>
</EuiPage>
</>
<EuiPage>
<EuiPageSideBar>
<EuiSideNav
toggleOpenOnMobile={() => toggleOpenOnMobile()}
isOpenOnMobile={isSideNavOpenOnMobile}
items={[
{
name: i18n.translate('xpack.apm.settings.pageTitle', {
defaultMessage: 'Settings',
}),
id: 0,
items: [
{
name: i18n.translate('xpack.apm.settings.agentConfig', {
defaultMessage: 'Agent Configuration',
}),
id: '1',
href: getSettingsHref('/agent-configuration'),
isSelected: pathname.startsWith(
'/settings/agent-configuration'
),
},
...(canAccessML
? [
{
name: i18n.translate(
'xpack.apm.settings.anomalyDetection',
{
defaultMessage: 'Anomaly detection',
}
),
id: '4',
href: getSettingsHref('/anomaly-detection'),
isSelected: pathname === '/settings/anomaly-detection',
},
]
: []),
{
name: i18n.translate('xpack.apm.settings.customizeApp', {
defaultMessage: 'Customize app',
}),
id: '3',
href: getSettingsHref('/customize-ui'),
isSelected: pathname === '/settings/customize-ui',
},
{
name: i18n.translate('xpack.apm.settings.indices', {
defaultMessage: 'Indices',
}),
id: '2',
href: getSettingsHref('/apm-indices'),
isSelected: pathname === '/settings/apm-indices',
},
],
},
]}
/>
</EuiPageSideBar>
<EuiPageBody>{children}</EuiPageBody>
</EuiPage>
);
}

View file

@ -7,7 +7,7 @@
import {
EuiFlexGroup,
EuiPage,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTitle,
@ -18,7 +18,6 @@ import { useTrackPageview } from '../../../../../observability/public';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
import { SearchBar } from '../../shared/search_bar';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
import { ErrorGroupList } from './List';
@ -68,41 +67,41 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 });
if (!errorDistributionData || !errorGroupListData) {
return <SearchBar />;
return null;
}
return (
<>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiPanel>
<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
{
defaultMessage: 'Error occurrences',
}
)}
/>
</EuiPanel>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiPanel>
<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.serviceDetails.metrics.errorOccurrencesChart.title',
{ defaultMessage: 'Error occurrences' }
)}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>
<EuiTitle size="xs">
<h3>
{i18n.translate(
'xpack.apm.serviceDetails.metrics.errorsList.title',
{ defaultMessage: 'Errors' }
)}
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiPanel>
<EuiTitle size="xs">
<h3>Errors</h3>
</EuiTitle>
<EuiSpacer size="s" />
<ErrorGroupList
items={errorGroupListData.errorGroups}
serviceName={serviceName}
/>
</EuiPanel>
</EuiFlexGroup>
</EuiPage>
</>
<ErrorGroupList
items={errorGroupListData.errorGroups}
serviceName={serviceName}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -1,39 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { ApmHeader } from '../../shared/ApmHeader';
import { ServiceIcons } from './service_icons';
import { ServiceDetailTabs } from './service_detail_tabs';
interface Props extends RouteComponentProps<{ serviceName: string }> {
tab: React.ComponentProps<typeof ServiceDetailTabs>['tab'];
}
export function ServiceDetails({ match, tab }: Props) {
const { serviceName } = match.params;
return (
<div>
<ApmHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle>
<h1>{serviceName}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ServiceIcons serviceName={serviceName} />
</EuiFlexItem>
</EuiFlexGroup>
</ApmHeader>
<ServiceDetailTabs serviceName={serviceName} tab={tab} />
</div>
);
}

View file

@ -1,202 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiTab } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { ReactNode } from 'react';
import { EuiBetaBadge } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name';
import { enableServiceOverview } from '../../../../common/ui_settings_keys';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink';
import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink';
import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink';
import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink';
import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link';
import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link';
import { useServiceProfilingHref } from '../../shared/Links/apm/service_profiling_link';
import { MainTabs } from '../../shared/main_tabs';
import { ErrorGroupOverview } from '../error_group_overview';
import { ServiceMap } from '../ServiceMap';
import { ServiceNodeOverview } from '../service_node_overview';
import { ServiceMetrics } from '../service_metrics';
import { ServiceOverview } from '../service_overview';
import { TransactionOverview } from '../transaction_overview';
import { ServiceProfiling } from '../service_profiling';
import { Correlations } from '../correlations';
interface Tab {
key: string;
href: string;
text: ReactNode;
hidden?: boolean;
render: () => ReactNode;
}
interface Props {
serviceName: string;
tab:
| 'errors'
| 'metrics'
| 'nodes'
| 'overview'
| 'service-map'
| 'profiling'
| 'transactions';
}
export function ServiceDetailTabs({ serviceName, tab }: Props) {
const { agentName, transactionType } = useApmServiceContext();
const {
core: { uiSettings },
config,
} = useApmPluginContext();
const {
urlParams: { latencyAggregationType },
} = useUrlParams();
const overviewTab = {
key: 'overview',
href: useServiceOverviewHref({ serviceName, transactionType }),
text: i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', {
defaultMessage: 'Overview',
}),
render: () => (
<ServiceOverview agentName={agentName} serviceName={serviceName} />
),
};
const transactionsTab = {
key: 'transactions',
href: useTransactionsOverviewHref({
serviceName,
latencyAggregationType,
transactionType,
}),
text: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
defaultMessage: 'Transactions',
}),
render: () => <TransactionOverview serviceName={serviceName} />,
};
const errorsTab = {
key: 'errors',
href: useErrorOverviewHref(serviceName),
text: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
defaultMessage: 'Errors',
}),
render: () => {
return <ErrorGroupOverview serviceName={serviceName} />;
},
};
const serviceMapTab = {
key: 'service-map',
href: useServiceMapHref(serviceName),
text: i18n.translate('xpack.apm.home.serviceMapTabLabel', {
defaultMessage: 'Service Map',
}),
render: () => <ServiceMap serviceName={serviceName} />,
};
const nodesListTab = {
key: 'nodes',
href: useServiceNodeOverviewHref(serviceName),
text: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', {
defaultMessage: 'JVMs',
}),
render: () => <ServiceNodeOverview serviceName={serviceName} />,
};
const metricsTab = {
key: 'metrics',
href: useMetricOverviewHref(serviceName),
text: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
defaultMessage: 'Metrics',
}),
render: () =>
agentName ? (
<ServiceMetrics agentName={agentName} serviceName={serviceName} />
) : null,
};
const profilingTab = {
key: 'profiling',
href: useServiceProfilingHref({ serviceName }),
hidden: !config.profilingEnabled,
text: (
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem>
{i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', {
defaultMessage: 'Profiling',
})}
</EuiFlexItem>
<EuiFlexItem>
<EuiBetaBadge
label={i18n.translate(
'xpack.apm.serviceDetails.profilingTabExperimentalLabel',
{
defaultMessage: 'Experimental',
}
)}
tooltipContent={i18n.translate(
'xpack.apm.serviceDetails.profilingTabExperimentalDescription',
{
defaultMessage:
'Profiling is highly experimental and for internal use only.',
}
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
render: () => <ServiceProfiling serviceName={serviceName} />,
};
const tabs: Tab[] = [transactionsTab, errorsTab];
if (uiSettings.get(enableServiceOverview)) {
tabs.unshift(overviewTab);
}
if (isJavaAgentName(agentName)) {
tabs.push(nodesListTab);
} else if (agentName && !isRumAgentName(agentName)) {
tabs.push(metricsTab);
}
tabs.push(serviceMapTab, profilingTab);
const selectedTab = tabs.find((serviceTab) => serviceTab.key === tab);
return (
<>
<MainTabs>
{tabs
.filter((t) => !t.hidden)
.map(({ href, key, text }) => (
<EuiTab
data-test-subj={`tab_${key}`}
href={href}
isSelected={key === tab}
key={key}
>
{text}
</EuiTab>
))}
<div style={{ marginLeft: 'auto' }}>
<Correlations />
</div>
</MainTabs>
{selectedTab ? selectedTab.render() : null}
</>
);
}

View file

@ -5,13 +5,7 @@
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiPage,
EuiPanel,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect } from 'react';
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
@ -126,28 +120,26 @@ export function ServiceInventory() {
return (
<>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
{displayMlCallout ? (
<EuiFlexItem>
<MLCallout onDismiss={() => setUserHasDismissedCallout(true)} />
</EuiFlexItem>
) : null}
<EuiFlexGroup direction="column" gutterSize="s">
{displayMlCallout && (
<EuiFlexItem>
<EuiPanel>
<ServiceList
items={servicesData.items}
noItemsMessage={
<NoServicesMessage
historicalDataFound={servicesData.hasHistoricalData}
status={servicesStatus}
/>
}
/>
</EuiPanel>
<MLCallout onDismiss={() => setUserHasDismissedCallout(true)} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPage>
)}
<EuiFlexItem>
<EuiPanel hasShadow={false}>
<ServiceList
items={servicesData.items}
noItemsMessage={
<NoServicesMessage
historicalDataFound={servicesData.hasHistoricalData}
status={servicesStatus}
/>
}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}

View file

@ -13,11 +13,11 @@ import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock
import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider';
import { createCallApmApi } from '../../../../services/rest/createCallApmApi';
import { CytoscapeContext } from '../Cytoscape';
import { Popover } from './';
import { Popover } from '.';
import exampleGroupedConnectionsData from '../__stories__/example_grouped_connections.json';
export default {
title: 'app/ServiceMap/Popover',
title: 'app/service_map/Popover',
component: Popover,
decorators: [
(Story: ComponentType) => {

View file

@ -10,7 +10,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea
import { ServiceStatsList } from './ServiceStatsList';
export default {
title: 'app/ServiceMap/Popover/ServiceStatsList',
title: 'app/service_map/Popover/ServiceStatsList',
component: ServiceStatsList,
decorators: [
(Story: ComponentType) => (

View file

@ -14,7 +14,7 @@ import { iconForNode } from '../icons';
import { Centerer } from './centerer';
export default {
title: 'app/ServiceMap/Cytoscape',
title: 'app/service_map/Cytoscape',
component: Cytoscape,
decorators: [
(Story: ComponentType) => (

View file

@ -25,7 +25,7 @@ import exampleResponseOpbeansBeats from './example_response_opbeans_beats.json';
import exampleResponseTodo from './example_response_todo.json';
import { generateServiceMapElements } from './generate_service_map_elements';
const STORYBOOK_PATH = 'app/ServiceMap/Cytoscape/Example data';
const STORYBOOK_PATH = 'app/service_map/Cytoscape/Example data';
const SESSION_STORAGE_KEY = `${STORYBOOK_PATH}/pre-loaded map`;
function getSessionJson() {
@ -40,7 +40,7 @@ function getHeight() {
}
export default {
title: 'app/ServiceMap/Cytoscape/Example data',
title: 'app/service_map/Cytoscape/Example data',
component: Cytoscape,
decorators: [
(Story: ComponentType) => (

View file

@ -15,7 +15,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
import { LicenseContext } from '../../../context/license/license_context';
import * as useFetcherModule from '../../../hooks/use_fetcher';
import { ServiceMap } from './';
import { ServiceMap } from '.';
import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context';
import { Router } from 'react-router-dom';

View file

@ -7,7 +7,6 @@
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import React, { PropsWithChildren, ReactNode } from 'react';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { isActivePlatinumLicense } from '../../../../common/license_check';
import { useTrackPageview } from '../../../../../observability/public';
import {
@ -18,7 +17,6 @@ import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useLicenseContext } from '../../../context/license/use_license_context';
import { useTheme } from '../../../hooks/use_theme';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { DatePicker } from '../../shared/DatePicker';
import { LicensePrompt } from '../../shared/license_prompt';
import { Controls } from './Controls';
import { Cytoscape } from './Cytoscape';
@ -28,31 +26,16 @@ import { EmptyPrompt } from './empty_prompt';
import { Popover } from './Popover';
import { TimeoutPrompt } from './timeout_prompt';
import { useRefDimensions } from './useRefDimensions';
import { SearchBar } from '../../shared/search_bar';
interface ServiceMapProps {
serviceName?: string;
}
const ServiceMapDatePickerFlexGroup = euiStyled(EuiFlexGroup)`
padding: ${({ theme }) => theme.eui.euiSizeM};
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
margin: 0;
`;
function DatePickerSection() {
return (
<ServiceMapDatePickerFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<DatePicker />
</EuiFlexItem>
</ServiceMapDatePickerFlexGroup>
);
}
function PromptContainer({ children }: { children: ReactNode }) {
return (
<>
<DatePickerSection />
<SearchBar showKueryBar={false} />
<EuiFlexGroup
alignItems="center"
justifyContent="spaceAround"
@ -153,7 +136,8 @@ export function ServiceMap({
return (
<>
<DatePickerSection />
<SearchBar showKueryBar={false} />
<div data-test-subj="ServiceMap" style={{ height }} ref={ref}>
<Cytoscape
elements={data.elements}

View file

@ -5,30 +5,14 @@
* 2.0.
*/
import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher';
import { MetricsChart } from '../../shared/charts/metrics_chart';
import { SearchBar } from '../../shared/search_bar';
interface ServiceMetricsProps {
agentName: string;
serviceName: string;
}
export function ServiceMetrics({
agentName,
serviceName,
}: ServiceMetricsProps) {
export function ServiceMetrics() {
const { urlParams } = useUrlParams();
const { data, status } = useServiceMetricChartsFetcher({
serviceNodeName: undefined,
@ -36,29 +20,22 @@ export function ServiceMetrics({
const { start, end } = urlParams;
return (
<>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<ChartPointerEventContextProvider>
<EuiFlexGrid columns={2} gutterSize="s">
{data.charts.map((chart) => (
<EuiFlexItem key={chart.key}>
<EuiPanel>
<MetricsChart
start={start}
end={end}
chart={chart}
fetchStatus={status}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGrid>
<EuiSpacer size="xxl" />
</ChartPointerEventContextProvider>
</EuiFlexGroup>
</EuiPage>
</>
<ChartPointerEventContextProvider>
<EuiFlexGrid columns={2} gutterSize="s">
{data.charts.map((chart) => (
<EuiFlexItem key={chart.key}>
<EuiPanel>
<MetricsChart
start={start}
end={end}
chart={chart}
fetchStatus={status}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGrid>
<EuiSpacer size="xxl" />
</ChartPointerEventContextProvider>
);
}

View file

@ -9,20 +9,17 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ServiceNodeMetrics } from '.';
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
import { RouteComponentProps } from 'react-router-dom';
describe('ServiceNodeMetrics', () => {
describe('render', () => {
it('renders', () => {
const props = ({} as unknown) as RouteComponentProps<{
serviceName: string;
serviceNodeName: string;
}>;
expect(() =>
shallow(
<MockApmPluginContextWrapper>
<ServiceNodeMetrics {...props} />
<ServiceNodeMetrics
serviceName="my-service"
serviceNodeName="node-name"
/>
</MockApmPluginContextWrapper>
)
).not.toThrowError();

View file

@ -10,17 +10,14 @@ import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPanel,
EuiSpacer,
EuiStat,
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
@ -29,10 +26,8 @@ import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { px, truncate, unit } from '../../../style/variables';
import { ApmHeader } from '../../shared/ApmHeader';
import { MetricsChart } from '../../shared/charts/metrics_chart';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { SearchBar } from '../../shared/search_bar';
const INITIAL_DATA = {
host: '',
@ -51,16 +46,18 @@ const MetadataFlexGroup = euiStyled(EuiFlexGroup)`
`${theme.eui.paddingSizes.m} 0 0 ${theme.eui.paddingSizes.m}`};
`;
type ServiceNodeMetricsProps = RouteComponentProps<{
interface ServiceNodeMetricsProps {
serviceName: string;
serviceNodeName: string;
}>;
}
export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) {
export function ServiceNodeMetrics({
serviceName,
serviceNodeName,
}: ServiceNodeMetricsProps) {
const {
urlParams: { kuery, start, end },
} = useUrlParams();
const { serviceName, serviceNodeName } = match.params;
const { agentName } = useApmServiceContext();
const { data } = useServiceMetricChartsFetcher({ serviceNodeName });
@ -89,15 +86,6 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) {
return (
<>
<ApmHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle>
<h1>{serviceName}</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</ApmHeader>
{isAggregatedData ? (
<EuiCallOut
title={i18n.translate(
@ -179,28 +167,26 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) {
</EuiFlexItem>
</MetadataFlexGroup>
)}
<SearchBar />
<EuiPage>
{agentName && (
<ChartPointerEventContextProvider>
<EuiFlexGrid columns={2} gutterSize="s">
{data.charts.map((chart) => (
<EuiFlexItem key={chart.key}>
<EuiPanel>
<MetricsChart
start={start}
end={end}
chart={chart}
fetchStatus={status}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGrid>
<EuiSpacer size="xxl" />
</ChartPointerEventContextProvider>
)}
</EuiPage>
{agentName && (
<ChartPointerEventContextProvider>
<EuiFlexGrid columns={2} gutterSize="s">
{data.charts.map((chart) => (
<EuiFlexItem key={chart.key}>
<EuiPanel>
<MetricsChart
start={start}
end={end}
chart={chart}
fetchStatus={status}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGrid>
<EuiSpacer size="xxl" />
</ChartPointerEventContextProvider>
)}
</>
);
}

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiPage, EuiPanel, EuiToolTip } from '@elastic/eui';
import { EuiPanel, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
@ -22,7 +22,6 @@ import { useFetcher } from '../../../hooks/use_fetcher';
import { px, truncate, unit } from '../../../style/variables';
import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink';
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
import { SearchBar } from '../../shared/search_bar';
const INITIAL_PAGE_SIZE = 25;
const INITIAL_SORT_FIELD = 'cpu';
@ -143,28 +142,18 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
];
return (
<>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiPanel>
<ManagedTable
noItemsMessage={i18n.translate(
'xpack.apm.jvmsTable.noJvmsLabel',
{
defaultMessage: 'No JVMs were found',
}
)}
items={items}
columns={columns}
initialPageSize={INITIAL_PAGE_SIZE}
initialSortField={INITIAL_SORT_FIELD}
initialSortDirection={INITIAL_SORT_DIRECTION}
/>
</EuiPanel>
</EuiFlexGroup>
</EuiPage>
</>
<EuiPanel hasShadow={false}>
<ManagedTable
noItemsMessage={i18n.translate('xpack.apm.jvmsTable.noJvmsLabel', {
defaultMessage: 'No JVMs were found',
})}
items={items}
columns={columns}
initialPageSize={INITIAL_PAGE_SIZE}
initialSortField={INITIAL_SORT_FIELD}
initialSortDirection={INITIAL_SORT_DIRECTION}
/>
</EuiPanel>
);
}

View file

@ -5,17 +5,17 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
import { isRumAgentName } from '../../../../common/agent_name';
import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useBreakPoints } from '../../../hooks/use_break_points';
import { LatencyChart } from '../../shared/charts/latency_chart';
import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart';
import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart';
import { SearchBar } from '../../shared/search_bar';
import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table';
import { ServiceOverviewErrorsTable } from './service_overview_errors_table';
import { ServiceOverviewInstancesChartAndTable } from './service_overview_instances_chart_and_table';
@ -29,14 +29,12 @@ import { ServiceOverviewTransactionsTable } from './service_overview_transaction
export const chartHeight = 288;
interface ServiceOverviewProps {
agentName?: string;
serviceName: string;
}
export function ServiceOverview({
agentName,
serviceName,
}: ServiceOverviewProps) {
export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
const { agentName } = useApmServiceContext();
useTrackPageview({ app: 'apm', path: 'service_overview' });
useTrackPageview({ app: 'apm', path: 'service_overview', delay: 15000 });
@ -49,89 +47,84 @@ export function ServiceOverview({
return (
<AnnotationsContextProvider>
<ChartPointerEventContextProvider>
<SearchBar showTransactionTypeSelector showTimeComparison />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiPanel>
<LatencyChart height={200} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
direction={rowDirection}
gutterSize="s"
responsive={false}
>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiPanel>
<LatencyChart height={200} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
direction={rowDirection}
gutterSize="s"
responsive={false}
>
<EuiFlexItem grow={3}>
<ServiceOverviewThroughputChart height={chartHeight} />
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiPanel>
<ServiceOverviewTransactionsTable serviceName={serviceName} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
direction={rowDirection}
gutterSize="s"
responsive={false}
>
{!isRumAgent && (
<EuiFlexItem grow={3}>
<ServiceOverviewThroughputChart height={chartHeight} />
<TransactionErrorRateChart
height={chartHeight}
showAnnotations={false}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={7}>
<EuiPanel>
<ServiceOverviewErrorsTable serviceName={serviceName} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
direction={rowDirection}
gutterSize="s"
responsive={false}
>
<EuiFlexItem grow={3}>
<TransactionBreakdownChart showAnnotations={false} />
</EuiFlexItem>
{!isRumAgent && (
<EuiFlexItem grow={7}>
<EuiPanel>
<ServiceOverviewTransactionsTable
<ServiceOverviewDependenciesTable
serviceName={serviceName}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{!isRumAgent && (
<EuiFlexItem>
<EuiFlexGroup
direction={rowDirection}
direction="column"
gutterSize="s"
responsive={false}
>
{!isRumAgent && (
<EuiFlexItem grow={3}>
<TransactionErrorRateChart
height={chartHeight}
showAnnotations={false}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={7}>
<EuiPanel>
<ServiceOverviewErrorsTable serviceName={serviceName} />
</EuiPanel>
</EuiFlexItem>
<ServiceOverviewInstancesChartAndTable
chartHeight={chartHeight}
serviceName={serviceName}
/>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
direction={rowDirection}
gutterSize="s"
responsive={false}
>
<EuiFlexItem grow={3}>
<TransactionBreakdownChart showAnnotations={false} />
</EuiFlexItem>
{!isRumAgent && (
<EuiFlexItem grow={7}>
<EuiPanel>
<ServiceOverviewDependenciesTable
serviceName={serviceName}
/>
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{!isRumAgent && (
<EuiFlexItem>
<EuiFlexGroup
direction="column"
gutterSize="s"
responsive={false}
>
<ServiceOverviewInstancesChartAndTable
chartHeight={chartHeight}
serviceName={serviceName}
/>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPage>
)}
</EuiFlexGroup>
</ChartPointerEventContextProvider>
</AnnotationsContextProvider>
);

View file

@ -234,6 +234,7 @@ export function getColumns({
anchorPosition="leftCenter"
button={
<EuiButtonIcon
aria-label="Edit"
data-test-subj={`instanceActionsButton_${instanceItem.serviceNodeName}`}
iconType="boxesHorizontal"
onClick={() =>

View file

@ -32,10 +32,7 @@ import { pct } from '../../../../style/variables';
import { getAgentIcon } from '../../../shared/AgentIcon/get_agent_icon';
import { KeyValueFilterList } from '../../../shared/key_value_filter_list';
import { pushNewItemToKueryBar } from '../../../shared/KueryBar/utils';
import {
getCloudIcon,
getContainerIcon,
} from '../../service_details/service_icons';
import { getCloudIcon, getContainerIcon } from '../../../shared/service_icons';
import { useInstanceDetailsFetcher } from './use_instance_details_fetcher';
type ServiceInstanceDetails = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>;

View file

@ -4,14 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import {
getValueTypeConfig,
@ -20,7 +13,6 @@ import {
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useFetcher } from '../../../hooks/use_fetcher';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { SearchBar } from '../../shared/search_bar';
import { ServiceProfilingFlamegraph } from './service_profiling_flamegraph';
import { ServiceProfilingTimeline } from './service_profiling_timeline';
@ -90,54 +82,38 @@ export function ServiceProfiling({
return (
<>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>
{i18n.translate('xpack.apm.profilingOverviewTitle', {
defaultMessage: 'Profiling',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiPanel>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiPanel>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<ServiceProfilingTimeline
start={start!}
end={end!}
series={profilingTimeline}
onValueTypeSelect={(type) => {
setValueType(type);
}}
selectedValueType={valueType}
/>
</EuiFlexItem>
{valueType ? (
<EuiFlexItem>
<EuiTitle size="s">
<h3>{getValueTypeConfig(valueType).label}</h3>
</EuiTitle>
</EuiFlexItem>
) : null}
<EuiFlexItem>
<ServiceProfilingFlamegraph
serviceName={serviceName}
environment={environment}
valueType={valueType}
start={start}
end={end}
kuery={kuery}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<ServiceProfilingTimeline
start={start!}
end={end!}
series={profilingTimeline}
onValueTypeSelect={(type) => {
setValueType(type);
}}
selectedValueType={valueType}
/>
</EuiFlexItem>
{valueType ? (
<EuiFlexItem>
<EuiTitle size="s">
<h3>{getValueTypeConfig(valueType).label}</h3>
</EuiTitle>
</EuiFlexItem>
) : null}
<EuiFlexItem>
<ServiceProfilingFlamegraph
serviceName={serviceName}
environment={environment}
valueType={valueType}
start={start}
end={end}
kuery={kuery}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPage>
</EuiPanel>
</>
);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiFlexGroup, EuiPage, EuiPanel } from '@elastic/eui';
import { EuiPanel } from '@elastic/eui';
import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
@ -50,16 +50,13 @@ export function TraceOverview() {
return (
<>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiPanel>
<TraceList
items={data.items}
isLoading={status === FETCH_STATUS.LOADING}
/>
</EuiPanel>
</EuiFlexGroup>
</EuiPage>
<EuiPanel hasShadow={false}>
<TraceList
items={data.items}
isLoading={status === FETCH_STATUS.LOADING}
/>
</EuiPanel>
</>
);
}

View file

@ -7,7 +7,6 @@
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 { LogStream } from '../../../../../../infra/public';
@ -19,7 +18,6 @@ import { WaterfallContainer } from './WaterfallContainer';
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
interface Props {
location: Location;
transaction: Transaction;
urlParams: IUrlParams;
waterfall: IWaterfall;
@ -27,7 +25,6 @@ interface Props {
}
export function TransactionTabs({
location,
transaction,
urlParams,
waterfall,
@ -47,9 +44,9 @@ export function TransactionTabs({
<EuiTab
onClick={() => {
history.replace({
...location,
...history.location,
search: fromQuery({
...toQuery(location.search),
...toQuery(history.location.search),
detailTab: key,
}),
});
@ -66,7 +63,6 @@ export function TransactionTabs({
<EuiSpacer />
<TabContent
location={location}
urlParams={urlParams}
waterfall={waterfall}
exceedsMax={exceedsMax}
@ -101,19 +97,16 @@ const logsTab = {
};
function TimelineTabContent({
location,
urlParams,
waterfall,
exceedsMax,
}: {
location: Location<any>;
urlParams: IUrlParams;
waterfall: IWaterfall;
exceedsMax: boolean;
}) {
return (
<WaterfallContainer
location={location}
urlParams={urlParams}
waterfall={waterfall}
exceedsMax={exceedsMax}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { History, Location } from 'history';
import { History } from 'history';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { SpanFlyout } from './SpanFlyout';
@ -15,20 +15,12 @@ import { IWaterfall } from './waterfall_helpers/waterfall_helpers';
interface Props {
waterfallItemId?: string;
waterfall: IWaterfall;
location: Location;
toggleFlyout: ({
history,
location,
}: {
history: History;
location: Location;
}) => void;
toggleFlyout: ({ history }: { history: History }) => void;
}
export function WaterfallFlyout({
waterfallItemId,
waterfall,
location,
toggleFlyout,
}: Props) {
const history = useHistory();
@ -52,14 +44,14 @@ export function WaterfallFlyout({
totalDuration={waterfall.duration}
span={currentItem.doc}
parentTransaction={parentTransaction}
onClose={() => toggleFlyout({ history, location })}
onClose={() => toggleFlyout({ history })}
/>
);
case 'transaction':
return (
<TransactionFlyout
transaction={currentItem.doc}
onClose={() => toggleFlyout({ history, location })}
onClose={() => toggleFlyout({ history })}
rootTransactionDuration={
waterfall.rootTransaction?.transaction.duration.us
}

View file

@ -6,7 +6,6 @@
*/
import { EuiAccordion, EuiAccordionProps } from '@elastic/eui';
import { Location } from 'history';
import { isEmpty } from 'lodash';
import React, { useState } from 'react';
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
@ -23,7 +22,6 @@ interface AccordionWaterfallProps {
level: number;
duration: IWaterfall['duration'];
waterfallItemId?: string;
location: Location;
errorsPerTransaction: IWaterfall['errorsPerTransaction'];
childrenByParentId: Record<string, IWaterfallSpanOrTransaction[]>;
onToggleEntryTransaction?: () => void;
@ -100,7 +98,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
duration,
childrenByParentId,
waterfallItemId,
location,
errorsPerTransaction,
timelineMargins,
onClickWaterfallItem,
@ -160,7 +157,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
item={child}
level={nextLevel}
waterfallItemId={waterfallItemId}
location={location}
errorsPerTransaction={errorsPerTransaction}
duration={duration}
childrenByParentId={childrenByParentId}

View file

@ -7,7 +7,7 @@
import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { History, Location } from 'history';
import { History } from 'history';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
@ -40,14 +40,12 @@ const TIMELINE_MARGINS = {
const toggleFlyout = ({
history,
item,
location,
}: {
history: History;
item?: IWaterfallItem;
location: Location;
}) => {
history.replace({
...location,
...history.location,
search: fromQuery({
...toQuery(location.search),
flyoutDetailTab: undefined,
@ -63,15 +61,9 @@ const WaterfallItemsContainer = euiStyled.div`
interface Props {
waterfallItemId?: string;
waterfall: IWaterfall;
location: Location;
exceedsMax: boolean;
}
export function Waterfall({
waterfall,
exceedsMax,
waterfallItemId,
location,
}: Props) {
export function Waterfall({ waterfall, exceedsMax, waterfallItemId }: 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
@ -97,13 +89,12 @@ export function Waterfall({
item={entryWaterfallTransaction}
level={0}
waterfallItemId={waterfallItemId}
location={location}
errorsPerTransaction={waterfall.errorsPerTransaction}
duration={duration}
childrenByParentId={childrenByParentId}
timelineMargins={TIMELINE_MARGINS}
onClickWaterfallItem={(item: IWaterfallItem) =>
toggleFlyout({ history, item, location })
toggleFlyout({ history, item })
}
onToggleEntryTransaction={() => setIsAccordionOpen((isOpen) => !isOpen)}
/>
@ -148,7 +139,6 @@ export function Waterfall({
<WaterfallFlyout
waterfallItemId={waterfallItemId}
waterfall={waterfall}
location={location}
toggleFlyout={toggleFlyout}
/>
</Container>

View file

@ -15,7 +15,6 @@ import { WaterfallContainer } from './index';
import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
import {
inferredSpans,
location,
simpleTrace,
traceChildStartBeforeParent,
traceWithErrors,
@ -45,7 +44,6 @@ export function Example() {
);
return (
<WaterfallContainer
location={location}
urlParams={urlParams}
waterfall={waterfall}
exceedsMax={false}
@ -60,7 +58,6 @@ export function WithErrors() {
);
return (
<WaterfallContainer
location={location}
urlParams={urlParams}
waterfall={waterfall}
exceedsMax={false}
@ -75,7 +72,6 @@ export function ChildStartsBeforeParent() {
);
return (
<WaterfallContainer
location={location}
urlParams={urlParams}
waterfall={waterfall}
exceedsMax={false}
@ -90,7 +86,6 @@ export function InferredSpans() {
);
return (
<WaterfallContainer
location={location}
urlParams={urlParams}
waterfall={waterfall}
exceedsMax={false}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { Location } from 'history';
import React from 'react';
import { keyBy } from 'lodash';
import { useParams } from 'react-router-dom';
@ -19,13 +18,11 @@ import { WaterfallLegends } from './WaterfallLegends';
interface Props {
urlParams: IUrlParams;
location: Location;
waterfall: IWaterfall;
exceedsMax: boolean;
}
export function WaterfallContainer({
location,
urlParams,
waterfall,
exceedsMax,
@ -84,7 +81,6 @@ export function WaterfallContainer({
<div>
<WaterfallLegends legends={legendsWithFallbackLabel} type={colorBy} />
<Waterfall
location={location}
waterfallItemId={urlParams.waterfallItemId}
waterfall={waterfall}
exceedsMax={exceedsMax}

View file

@ -15,7 +15,6 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
@ -34,7 +33,6 @@ type DistributionBucket = DistributionApiResponse['buckets'][0];
interface Props {
urlParams: IUrlParams;
location: Location;
waterfall: IWaterfall;
exceedsMax: boolean;
isLoading: boolean;
@ -43,7 +41,6 @@ interface Props {
export function WaterfallWithSummmary({
urlParams,
location,
waterfall,
exceedsMax,
isLoading,
@ -135,7 +132,6 @@ export function WaterfallWithSummmary({
<TransactionTabs
transaction={entryTransaction}
location={location}
urlParams={urlParams}
waterfall={waterfall}
exceedsMax={exceedsMax}

View file

@ -5,27 +5,18 @@
* 2.0.
*/
import {
EuiFlexGroup,
EuiHorizontalRule,
EuiPage,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { flatten, isEmpty } from 'lodash';
import React from 'react';
import { RouteComponentProps, useHistory } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher';
import { ApmHeader } from '../../shared/ApmHeader';
import { TransactionCharts } from '../../shared/charts/transaction_charts';
import { HeightRetainer } from '../../shared/HeightRetainer';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { SearchBar } from '../../shared/search_bar';
import { TransactionDistribution } from './Distribution';
import { useWaterfallFetcher } from './use_waterfall_fetcher';
import { WaterfallWithSummmary } from './WaterfallWithSummmary';
@ -35,12 +26,7 @@ interface Sample {
transactionId: string;
}
type TransactionDetailsProps = RouteComponentProps<{ serviceName: string }>;
export function TransactionDetails({
location,
match,
}: TransactionDetailsProps) {
export function TransactionDetails() {
const { urlParams } = useUrlParams();
const history = useHistory();
const {
@ -90,48 +76,43 @@ export function TransactionDetails({
return (
<>
<ApmHeader>
<EuiTitle>
<h1>{transactionName}</h1>
</EuiTitle>
</ApmHeader>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<ChartPointerEventContextProvider>
<TransactionCharts />
</ChartPointerEventContextProvider>
<EuiTitle>
<h2>{transactionName}</h2>
</EuiTitle>
<EuiHorizontalRule size="full" margin="l" />
<EuiSpacer size="s" />
<EuiPanel>
<TransactionDistribution
distribution={distributionData}
fetchStatus={distributionStatus}
urlParams={urlParams}
bucketIndex={bucketIndex}
onBucketClick={(bucket) => {
if (!isEmpty(bucket.samples)) {
selectSampleFromBucketClick(bucket.samples[0]);
}
}}
/>
</EuiPanel>
<ChartPointerEventContextProvider>
<TransactionCharts />
</ChartPointerEventContextProvider>
<EuiSpacer size="s" />
<EuiHorizontalRule size="full" margin="l" />
<HeightRetainer>
<WaterfallWithSummmary
location={location}
urlParams={urlParams}
waterfall={waterfall}
isLoading={waterfallStatus === FETCH_STATUS.LOADING}
exceedsMax={exceedsMax}
traceSamples={traceSamples}
/>
</HeightRetainer>
</EuiFlexGroup>
</EuiPage>
<EuiPanel>
<TransactionDistribution
distribution={distributionData}
fetchStatus={distributionStatus}
urlParams={urlParams}
bucketIndex={bucketIndex}
onBucketClick={(bucket) => {
if (!isEmpty(bucket.samples)) {
selectSampleFromBucketClick(bucket.samples[0]);
}
}}
/>
</EuiPanel>
<EuiSpacer size="s" />
<HeightRetainer>
<WaterfallWithSummmary
urlParams={urlParams}
waterfall={waterfall}
isLoading={waterfallStatus === FETCH_STATUS.LOADING}
exceedsMax={exceedsMax}
traceSamples={traceSamples}
/>
</HeightRetainer>
</>
);
}

View file

@ -8,8 +8,6 @@
import {
EuiCallOut,
EuiCode,
EuiFlexGroup,
EuiPage,
EuiPanel,
EuiSpacer,
EuiTitle,
@ -26,7 +24,6 @@ import { useUrlParams } from '../../../context/url_params_context/use_url_params
import { TransactionCharts } from '../../shared/charts/transaction_charts';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { SearchBar } from '../../shared/search_bar';
import { TransactionList } from './TransactionList';
import { useRedirect } from './useRedirect';
import { useTransactionListFetcher } from './use_transaction_list';
@ -80,62 +77,55 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
return (
<>
<SearchBar showTransactionTypeSelector />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<TransactionCharts />
<EuiSpacer size="s" />
<EuiPanel>
<EuiTitle size="xs">
<h3>Transactions</h3>
</EuiTitle>
<EuiSpacer size="s" />
{!transactionListData.isAggregationAccurate && (
<EuiCallOut
title={i18n.translate(
'xpack.apm.transactionCardinalityWarning.title',
{
defaultMessage:
'This view shows a subset of reported transactions.',
}
)}
color="danger"
iconType="alert"
>
<p>
<FormattedMessage
id="xpack.apm.transactionCardinalityWarning.body"
defaultMessage="The number of unique transaction names exceeds the configured value of {bucketSize}. Try reconfiguring your agents to group similar transactions or increase the value of {codeBlock}"
values={{
bucketSize: transactionListData.bucketSize,
codeBlock: (
<EuiCode>
xpack.apm.ui.transactionGroupBucketSize
</EuiCode>
),
}}
/>
<ElasticDocsLink
section="/kibana"
path="/troubleshooting.html#troubleshooting-too-many-transactions"
>
{i18n.translate(
'xpack.apm.transactionCardinalityWarning.docsLink',
{ defaultMessage: 'Learn more in the docs' }
)}
</ElasticDocsLink>
</p>
</EuiCallOut>
<TransactionCharts />
<EuiSpacer size="s" />
<EuiPanel>
<EuiTitle size="xs">
<h3>Transactions</h3>
</EuiTitle>
<EuiSpacer size="s" />
{!transactionListData.isAggregationAccurate && (
<EuiCallOut
title={i18n.translate(
'xpack.apm.transactionCardinalityWarning.title',
{
defaultMessage:
'This view shows a subset of reported transactions.',
}
)}
<EuiSpacer size="s" />
<TransactionList
isLoading={transactionListStatus === 'loading'}
items={transactionListData.items || []}
/>
</EuiPanel>
</EuiFlexGroup>
</EuiPage>
color="danger"
iconType="alert"
>
<p>
<FormattedMessage
id="xpack.apm.transactionCardinalityWarning.body"
defaultMessage="The number of unique transaction names exceeds the configured value of {bucketSize}. Try reconfiguring your agents to group similar transactions or increase the value of {codeBlock}"
values={{
bucketSize: transactionListData.bucketSize,
codeBlock: (
<EuiCode>xpack.apm.ui.transactionGroupBucketSize</EuiCode>
),
}}
/>
<ElasticDocsLink
section="/kibana"
path="/troubleshooting.html#troubleshooting-too-many-transactions"
>
{i18n.translate(
'xpack.apm.transactionCardinalityWarning.docsLink',
{ defaultMessage: 'Learn more in the docs' }
)}
</ElasticDocsLink>
</p>
</EuiCallOut>
)}
<EuiSpacer size="s" />
<TransactionList
isLoading={transactionListStatus === 'loading'}
items={transactionListData.items || []}
/>
</EuiPanel>
</>
);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { fireEvent, getByText, queryByLabelText } from '@testing-library/react';
import { queryByLabelText } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
@ -107,46 +107,6 @@ describe('TransactionOverview', () => {
const FILTER_BY_TYPE_LABEL = 'Transaction type';
describe('when transactionType is selected and multiple transaction types are given', () => {
it('renders a radio group with transaction types', () => {
const { container } = setup({
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
},
});
expect(getByText(container, 'firstType')).toBeInTheDocument();
expect(getByText(container, 'secondType')).toBeInTheDocument();
expect(getByText(container, 'firstType')).not.toBeNull();
});
it('should update the URL when a transaction type is selected', () => {
const { container } = setup({
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
},
});
expect(history.location.search).toEqual(
'?transactionType=secondType&rangeFrom=now-15m&rangeTo=now'
);
expect(getByText(container, 'firstType')).toBeInTheDocument();
expect(getByText(container, 'secondType')).toBeInTheDocument();
fireEvent.change(getByText(container, 'firstType').parentElement!, {
target: { value: 'firstType' },
});
expect(history.push).toHaveBeenCalled();
expect(history.location.search).toEqual(
'?transactionType=firstType&rangeFrom=now-15m&rangeTo=now'
);
});
});
describe('when a transaction type is selected, and there are no other transaction types', () => {
it('does not render a radio group with transaction types', () => {
const { container } = setup({

View file

@ -0,0 +1,478 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { getServiceNodeName } from '../../../common/service_nodes';
import { APMRouteDefinition } from '../../application/routes';
import { toQuery } from '../shared/Links/url_helpers';
import { ErrorGroupDetails } from '../app/ErrorGroupDetails';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { ServiceNodeMetrics } from '../app/service_node_metrics';
import { Settings } from '../app/Settings';
import { AgentConfigurations } from '../app/Settings/AgentConfigurations';
import { AnomalyDetection } from '../app/Settings/anomaly_detection';
import { ApmIndices } from '../app/Settings/ApmIndices';
import { CustomizeUI } from '../app/Settings/CustomizeUI';
import { TraceLink } from '../app/TraceLink';
import { TransactionDetails } from '../app/transaction_details';
import {
CreateAgentConfigurationRouteHandler,
EditAgentConfigurationRouteHandler,
} from './route_handlers/agent_configuration';
import { enableServiceOverview } from '../../../common/ui_settings_keys';
import { redirectTo } from './redirect_to';
import { ApmMainTemplate } from './templates/apm_main_template';
import { ApmServiceTemplate } from './templates/apm_service_template';
import { ServiceProfiling } from '../app/service_profiling';
import { ErrorGroupOverview } from '../app/error_group_overview';
import { ServiceMap } from '../app/service_map';
import { ServiceNodeOverview } from '../app/service_node_overview';
import { ServiceMetrics } from '../app/service_metrics';
import { ServiceOverview } from '../app/service_overview';
import { TransactionOverview } from '../app/transaction_overview';
import { ServiceInventory } from '../app/service_inventory';
import { TraceOverview } from '../app/trace_overview';
// These component function definitions are used below with the `component`
// property of the route definitions.
//
// If you provide an inline function to the component prop, you would create a
// new component every render. This results in the existing component unmounting
// and the new component mounting instead of just updating the existing component.
const ServiceInventoryTitle = i18n.translate(
'xpack.apm.views.serviceInventory.title',
{ defaultMessage: 'Services' }
);
function ServiceInventoryView() {
return (
<ApmMainTemplate pageTitle={ServiceInventoryTitle}>
<ServiceInventory />
</ApmMainTemplate>
);
}
const TraceOverviewTitle = i18n.translate(
'xpack.apm.views.traceOverview.title',
{
defaultMessage: 'Traces',
}
);
function TraceOverviewView() {
return (
<ApmMainTemplate pageTitle={TraceOverviewTitle}>
<TraceOverview />
</ApmMainTemplate>
);
}
const ServiceMapTitle = i18n.translate('xpack.apm.views.serviceMap.title', {
defaultMessage: 'Service Map',
});
function ServiceMapView() {
return (
<ApmMainTemplate pageTitle={ServiceMapTitle}>
<ServiceMap />
</ApmMainTemplate>
);
}
function ServiceDetailsErrorsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="errors">
<ErrorGroupOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ErrorGroupDetailsRouteView(
props: RouteComponentProps<{ serviceName: string; groupId: string }>
) {
const { serviceName, groupId } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="errors">
<ErrorGroupDetails serviceName={serviceName} groupId={groupId} />
</ApmServiceTemplate>
);
}
function ServiceDetailsMetricsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="metrics">
<ServiceMetrics />
</ApmServiceTemplate>
);
}
function ServiceDetailsNodesRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="nodes">
<ServiceNodeOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsOverviewRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate
serviceName={serviceName}
selectedTab="overview"
searchBarOptions={{
showTransactionTypeSelector: true,
showTimeComparison: true,
}}
>
<ServiceOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsServiceMapRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate
serviceName={serviceName}
selectedTab="service-map"
searchBarOptions={{ hidden: true }}
>
<ServiceMap serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsTransactionsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate
serviceName={serviceName}
selectedTab="transactions"
searchBarOptions={{
showTransactionTypeSelector: true,
}}
>
<TransactionOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsProfilingRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="profiling">
<ServiceProfiling serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceNodeMetricsRouteView(
props: RouteComponentProps<{
serviceName: string;
serviceNodeName: string;
}>
) {
const { serviceName, serviceNodeName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="nodes">
<ServiceNodeMetrics
serviceName={serviceName}
serviceNodeName={serviceNodeName}
/>
</ApmServiceTemplate>
);
}
function TransactionDetailsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="transactions">
<TransactionDetails />
</ApmServiceTemplate>
);
}
function SettingsAgentConfigurationRouteView() {
return (
<ApmMainTemplate pageTitle="Settings">
<Settings>
<AgentConfigurations />
</Settings>
</ApmMainTemplate>
);
}
function SettingsAnomalyDetectionRouteView() {
return (
<ApmMainTemplate pageTitle="Settings">
<Settings>
<AnomalyDetection />
</Settings>
</ApmMainTemplate>
);
}
function SettingsApmIndicesRouteView() {
return (
<ApmMainTemplate pageTitle="Settings">
<Settings>
<ApmIndices />
</Settings>
</ApmMainTemplate>
);
}
function SettingsCustomizeUI() {
return (
<ApmMainTemplate pageTitle="Settings">
<Settings>
<CustomizeUI />
</Settings>
</ApmMainTemplate>
);
}
const SettingsApmIndicesTitle = i18n.translate(
'xpack.apm.views.settings.indices.title',
{ defaultMessage: 'Indices' }
);
const SettingsAgentConfigurationTitle = i18n.translate(
'xpack.apm.views.settings.agentConfiguration.title',
{ defaultMessage: 'Agent Configuration' }
);
const CreateAgentConfigurationTitle = i18n.translate(
'xpack.apm.views.settings.createAgentConfiguration.title',
{ defaultMessage: 'Create Agent Configuration' }
);
const EditAgentConfigurationTitle = i18n.translate(
'xpack.apm.views.settings.editAgentConfiguration.title',
{ defaultMessage: 'Edit Agent Configuration' }
);
const SettingsCustomizeUITitle = i18n.translate(
'xpack.apm.views.settings.customizeUI.title',
{ defaultMessage: 'Customize app' }
);
const SettingsAnomalyDetectionTitle = i18n.translate(
'xpack.apm.views.settings.anomalyDetection.title',
{ defaultMessage: 'Anomaly detection' }
);
const SettingsTitle = i18n.translate('xpack.apm.views.listSettings.title', {
defaultMessage: 'Settings',
});
/**
* The array of route definitions to be used when the application
* creates the routes.
*/
export const apmRouteConfig: APMRouteDefinition[] = [
/*
* Home routes
*/
{
exact: true,
path: '/',
render: redirectTo('/services'),
breadcrumb: 'APM',
},
{
exact: true,
path: '/services', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
component: ServiceInventoryView,
breadcrumb: ServiceInventoryTitle,
},
{
exact: true,
path: '/traces', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
component: TraceOverviewView,
breadcrumb: TraceOverviewTitle,
},
{
exact: true,
path: '/service-map', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
component: ServiceMapView,
breadcrumb: ServiceMapTitle,
},
/*
* Settings routes
*/
{
exact: true,
path: '/settings',
render: redirectTo('/settings/agent-configuration'),
breadcrumb: SettingsTitle,
},
{
exact: true,
path: '/settings/agent-configuration',
component: SettingsAgentConfigurationRouteView,
breadcrumb: SettingsAgentConfigurationTitle,
},
{
exact: true,
path: '/settings/agent-configuration/create',
component: CreateAgentConfigurationRouteHandler,
breadcrumb: CreateAgentConfigurationTitle,
},
{
exact: true,
path: '/settings/agent-configuration/edit',
breadcrumb: EditAgentConfigurationTitle,
component: EditAgentConfigurationRouteHandler,
},
{
exact: true,
path: '/settings/apm-indices',
component: SettingsApmIndicesRouteView,
breadcrumb: SettingsApmIndicesTitle,
},
{
exact: true,
path: '/settings/customize-ui',
component: SettingsCustomizeUI,
breadcrumb: SettingsCustomizeUITitle,
},
{
exact: true,
path: '/settings/anomaly-detection',
component: SettingsAnomalyDetectionRouteView,
breadcrumb: SettingsAnomalyDetectionTitle,
},
/*
* Services routes (with APM Service context)
*/
{
exact: true,
path: '/services/:serviceName',
breadcrumb: ({ match }) => match.params.serviceName,
component: RedirectToDefaultServiceRouteView,
},
{
exact: true,
path: '/services/:serviceName/overview',
breadcrumb: i18n.translate('xpack.apm.views.overview.title', {
defaultMessage: 'Overview',
}),
component: ServiceDetailsOverviewRouteView,
},
{
exact: true,
path: '/services/:serviceName/transactions',
component: ServiceDetailsTransactionsRouteView,
breadcrumb: i18n.translate('xpack.apm.views.transactions.title', {
defaultMessage: 'Transactions',
}),
},
{
exact: true,
path: '/services/:serviceName/errors/:groupId',
component: ErrorGroupDetailsRouteView,
breadcrumb: ({ match }) => match.params.groupId,
},
{
exact: true,
path: '/services/:serviceName/errors',
component: ServiceDetailsErrorsRouteView,
breadcrumb: i18n.translate('xpack.apm.views.errors.title', {
defaultMessage: 'Errors',
}),
},
{
exact: true,
path: '/services/:serviceName/metrics',
component: ServiceDetailsMetricsRouteView,
breadcrumb: i18n.translate('xpack.apm.views.metrics.title', {
defaultMessage: 'Metrics',
}),
},
// service nodes, only enabled for java agents for now
{
exact: true,
path: '/services/:serviceName/nodes',
component: ServiceDetailsNodesRouteView,
breadcrumb: i18n.translate('xpack.apm.views.nodes.title', {
defaultMessage: 'JVMs',
}),
},
// node metrics
{
exact: true,
path: '/services/:serviceName/nodes/:serviceNodeName/metrics',
component: ServiceNodeMetricsRouteView,
breadcrumb: ({ match }) => getServiceNodeName(match.params.serviceNodeName),
},
{
exact: true,
path: '/services/:serviceName/transactions/view',
component: TransactionDetailsRouteView,
breadcrumb: ({ location }) => {
const query = toQuery(location.search);
return query.transactionName as string;
},
},
{
exact: true,
path: '/services/:serviceName/profiling',
component: ServiceDetailsProfilingRouteView,
breadcrumb: i18n.translate('xpack.apm.views.serviceProfiling.title', {
defaultMessage: 'Profiling',
}),
},
{
exact: true,
path: '/services/:serviceName/service-map',
component: ServiceDetailsServiceMapRouteView,
breadcrumb: i18n.translate('xpack.apm.views.serviceMap.title', {
defaultMessage: 'Service Map',
}),
},
/*
* Utilility routes
*/
{
exact: true,
path: '/link-to/trace/:traceId',
component: TraceLink,
breadcrumb: null,
},
];
function RedirectToDefaultServiceRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { uiSettings } = useApmPluginContext().core;
const { serviceName } = props.match.params;
if (uiSettings.get(enableServiceOverview)) {
return redirectTo(`/services/${serviceName}/overview`)(props);
}
return redirectTo(`/services/${serviceName}/transactions`)(props);
}

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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 { Route, Router, Switch } from 'react-router-dom';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
import {
KibanaContextProvider,
RedirectAppLinks,
useUiSetting$,
} from '../../../../../../src/plugins/kibana_react/public';
import { ScrollToTopOnPathChange } from '../../components/app/Main/ScrollToTopOnPathChange';
import {
ApmPluginContext,
ApmPluginContextValue,
} from '../../context/apm_plugin/apm_plugin_context';
import { LicenseProvider } from '../../context/license/license_context';
import { UrlParamsProvider } from '../../context/url_params_context/url_params_context';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { ApmPluginStartDeps } from '../../plugin';
import { HeaderMenuPortal } from '../../../../observability/public';
import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
import { apmRouteConfig } from './apm_route_config';
const MainContainer = euiStyled.div`
height: 100%;
`;
export function ApmAppRoot({
apmPluginContextValue,
pluginsStart,
}: {
apmPluginContextValue: ApmPluginContextValue;
pluginsStart: ApmPluginStartDeps;
}) {
const { appMountParameters, core } = apmPluginContextValue;
const { history } = appMountParameters;
const i18nCore = core.i18n;
return (
<RedirectAppLinks application={core.application}>
<ApmPluginContext.Provider value={apmPluginContextValue}>
<KibanaContextProvider services={{ ...core, ...pluginsStart }}>
<i18nCore.Context>
<Router history={history}>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<ApmThemeProvider>
<MainContainer
data-test-subj="apmMainContainer"
role="main"
>
<MountApmHeaderActionMenu />
<Route component={ScrollToTopOnPathChange} />
<Switch>
{apmRouteConfig.map((route, i) => (
<ApmRoute key={i} {...route} />
))}
</Switch>
</MainContainer>
</ApmThemeProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</ApmPluginContext.Provider>
</RedirectAppLinks>
);
}
function MountApmHeaderActionMenu() {
useBreadcrumbs(apmRouteConfig);
const { setHeaderActionMenu } = useApmPluginContext().appMountParameters;
return (
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu}>
<ApmHeaderActionMenu />
</HeaderMenuPortal>
);
}
function ApmThemeProvider({ children }: { children: React.ReactNode }) {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
return (
<ThemeProvider
theme={(outerTheme?: DefaultTheme) => ({
...outerTheme,
eui: darkMode ? euiDarkVars : euiLightVars,
darkMode,
})}
>
{children}
</ThemeProvider>
);
}

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
/**
* Given a path, redirect to that location, preserving the search and maintaining
* backward-compatibilty with legacy (pre-7.9) hash-based URLs.
*/
export function redirectTo(to: string) {
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 (
<Redirect
to={{
...location,
hash: '',
pathname: to,
search: resolvedUrl ? resolvedUrl.search : location.search,
}}
/>
);
};
}

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { routes } from './';
import { apmRouteConfig } from './apm_route_config';
describe('routes', () => {
describe('/', () => {
const route = routes.find((r) => r.path === '/');
const route = apmRouteConfig.find((r) => r.path === '/');
describe('with no hash path', () => {
it('redirects to /services', () => {

View file

@ -7,10 +7,10 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useFetcher } from '../../../../../hooks/use_fetcher';
import { toQuery } from '../../../../shared/Links/url_helpers';
import { Settings } from '../../../Settings';
import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurations/AgentConfigurationCreateEdit';
import { useFetcher } from '../../../hooks/use_fetcher';
import { toQuery } from '../../shared/Links/url_helpers';
import { Settings } from '../../app/Settings';
import { AgentConfigurationCreateEdit } from '../../app/Settings/AgentConfigurations/AgentConfigurationCreateEdit';
type EditAgentConfigurationRouteHandler = RouteComponentProps<{}>;

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ApmPluginStartDeps } from '../../../plugin';
import { EnvironmentFilter } from '../../shared/EnvironmentFilter';
/*
* This template contains:
* - The Shared Observability Nav (https://github.com/elastic/kibana/blob/f7698bd8aa8787d683c728300ba4ca52b202369c/x-pack/plugins/observability/public/components/shared/page_template/README.md)
* - The APM Header Action Menu
* - Page title
*
* Optionally:
* - EnvironmentFilter
*/
export function ApmMainTemplate({
pageTitle,
children,
}: {
pageTitle: React.ReactNode;
children: React.ReactNode;
}) {
const { services } = useKibana<ApmPluginStartDeps>();
const ObservabilityPageTemplate =
services.observability.navigation.PageTemplate;
return (
<ObservabilityPageTemplate
pageHeader={{
pageTitle,
rightSideItems: [<EnvironmentFilter />],
}}
>
{children}
</ObservabilityPageTemplate>
);
}

View file

@ -0,0 +1,216 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiTabs,
EuiTab,
EuiBetaBadge,
} from '@elastic/eui';
import { ApmMainTemplate } from './apm_main_template';
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
import { enableServiceOverview } from '../../../../common/ui_settings_keys';
import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name';
import { ServiceIcons } from '../../shared/service_icons';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink';
import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink';
import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink';
import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink';
import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link';
import { useServiceProfilingHref } from '../../shared/Links/apm/service_profiling_link';
import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { Correlations } from '../../app/correlations';
import { SearchBar } from '../../shared/search_bar';
interface Tab {
key: TabKey;
href: string;
text: React.ReactNode;
hidden?: boolean;
}
type TabKey =
| 'errors'
| 'metrics'
| 'nodes'
| 'overview'
| 'service-map'
| 'profiling'
| 'transactions';
export function ApmServiceTemplate({
children,
serviceName,
selectedTab,
searchBarOptions,
}: {
children: React.ReactNode;
serviceName: string;
selectedTab: TabKey;
searchBarOptions?: {
hidden?: boolean;
showTransactionTypeSelector?: boolean;
showTimeComparison?: boolean;
};
}) {
return (
<ApmMainTemplate
pageTitle={
<>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1>{serviceName}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ServiceIcons serviceName={serviceName} />
</EuiFlexItem>
</EuiFlexGroup>
</>
}
>
<ApmServiceContextProvider>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<TabNavigation
serviceName={serviceName}
selectedTab={selectedTab}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Correlations />
</EuiFlexItem>
</EuiFlexGroup>
<SearchBar {...searchBarOptions} />
{children}
</ApmServiceContextProvider>
</ApmMainTemplate>
);
}
function TabNavigation({
serviceName,
selectedTab,
}: {
serviceName: string;
selectedTab: TabKey;
}) {
const { agentName, transactionType } = useApmServiceContext();
const { core, config } = useApmPluginContext();
const { urlParams } = useUrlParams();
const tabs: Tab[] = [
{
key: 'overview',
href: useServiceOverviewHref({ serviceName, transactionType }),
text: i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', {
defaultMessage: 'Overview',
}),
hidden: !core.uiSettings.get(enableServiceOverview),
},
{
key: 'transactions',
href: useTransactionsOverviewHref({
serviceName,
latencyAggregationType: urlParams.latencyAggregationType,
transactionType,
}),
text: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
defaultMessage: 'Transactions',
}),
},
{
key: 'errors',
href: useErrorOverviewHref(serviceName),
text: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
defaultMessage: 'Errors',
}),
},
{
key: 'nodes',
href: useServiceNodeOverviewHref(serviceName),
text: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', {
defaultMessage: 'JVMs',
}),
hidden: !isJavaAgentName(agentName),
},
{
key: 'metrics',
href: useMetricOverviewHref(serviceName),
text: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
defaultMessage: 'Metrics',
}),
hidden:
!agentName || isRumAgentName(agentName) || isJavaAgentName(agentName),
},
{
key: 'service-map',
href: useServiceMapHref(serviceName),
text: i18n.translate('xpack.apm.home.serviceMapTabLabel', {
defaultMessage: 'Service Map',
}),
},
{
key: 'profiling',
href: useServiceProfilingHref({ serviceName }),
hidden: !config.profilingEnabled,
text: (
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem>
{i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', {
defaultMessage: 'Profiling',
})}
</EuiFlexItem>
<EuiFlexItem>
<EuiBetaBadge
label={i18n.translate(
'xpack.apm.serviceDetails.profilingTabExperimentalLabel',
{
defaultMessage: 'Experimental',
}
)}
tooltipContent={i18n.translate(
'xpack.apm.serviceDetails.profilingTabExperimentalDescription',
{
defaultMessage:
'Profiling is highly experimental and for internal use only.',
}
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
},
];
return (
<EuiTabs display="condensed">
{tabs
.filter((t) => !t.hidden)
.map(({ href, key, text }) => (
<EuiTab
data-test-subj={`tab_${key}`}
href={href}
isSelected={key === selectedTab}
key={key}
>
{text}
</EuiTab>
))}
</EuiTabs>
);
}

View file

@ -1,53 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiTitle } from '@elastic/eui';
import React, { ComponentType } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { CoreStart } from '../../../../../../../src/core/public';
import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import { createCallApmApi } from '../../../services/rest/createCallApmApi';
import { ApmHeader } from './';
export default {
title: 'shared/ApmHeader',
component: ApmHeader,
decorators: [
(Story: ComponentType) => {
createCallApmApi(({} as unknown) as CoreStart);
return (
<EuiThemeProvider>
<MockUrlParamsContextProvider
params={{ rangeFrom: 'now-15m', rangeTo: 'now' }}
>
<MockApmPluginContextWrapper>
<MemoryRouter>
<Story />
</MemoryRouter>
</MockApmPluginContextWrapper>
</MockUrlParamsContextProvider>
</EuiThemeProvider>
);
},
],
};
export function Example() {
return (
<ApmHeader>
<EuiTitle>
<h1>
GET
/api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all
</h1>
</EuiTitle>
</ApmHeader>
);
}

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { HeaderMenuPortal } from '../../../../../observability/public';
import { ActionMenu } from '../../../application/action_menu';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { EnvironmentFilter } from '../EnvironmentFilter';
const HeaderFlexGroup = euiStyled(EuiFlexGroup)`
padding: ${({ theme }) => theme.eui.gutterTypes.gutterMedium};
background: ${({ theme }) => theme.eui.euiColorEmptyShade};
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
`;
export function ApmHeader({ children }: { children: ReactNode }) {
const { setHeaderActionMenu } = useApmPluginContext().appMountParameters;
return (
<HeaderFlexGroup alignItems="center" gutterSize="s" wrap={true}>
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu}>
<ActionMenu />
</HeaderMenuPortal>
<EuiFlexItem>{children}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EnvironmentFilter />
</EuiFlexItem>
</HeaderFlexGroup>
);
}

View file

@ -13,6 +13,7 @@ import { useHistory, useLocation, useParams } from 'react-router-dom';
import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
omitEsFieldValue,
} from '../../../../common/environment_filter_values';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
@ -51,9 +52,9 @@ function getOptions(environments: string[]) {
}));
return [
ENVIRONMENT_ALL,
omitEsFieldValue(ENVIRONMENT_ALL),
...(environments.includes(ENVIRONMENT_NOT_DEFINED.value)
? [ENVIRONMENT_NOT_DEFINED]
? [omitEsFieldValue(ENVIRONMENT_NOT_DEFINED)]
: []),
...(environmentOptions.length > 0 ? [SEPARATOR_OPTION] : []),
...environmentOptions,
@ -78,12 +79,14 @@ export function EnvironmentFilter() {
// the contents.
const minWidth = 200;
const options = getOptions(environments);
return (
<EuiSelect
prepend={i18n.translate('xpack.apm.filter.environment.label', {
defaultMessage: 'Environment',
})}
options={getOptions(environments)}
options={options}
value={environment || ENVIRONMENT_ALL.value}
onChange={(event) => {
updateEnvironmentUrl(history, location, event.target.value);

View file

@ -13,9 +13,9 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { IBasePath } from '../../../../../../src/core/public';
import { AlertType } from '../../../common/alert_types';
import { AlertingFlyout } from '../../components/alerting/alerting_flyout';
import { IBasePath } from '../../../../../../../src/core/public';
import { AlertType } from '../../../../common/alert_types';
import { AlertingFlyout } from '../../alerting/alerting_flyout';
const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', {
defaultMessage: 'Alerts',

View file

@ -8,8 +8,8 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { MissingJobsAlert } from './anomaly_detection_setup_link';
import * as hooks from '../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { FETCH_STATUS } from '../../hooks/use_fetcher';
import * as hooks from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
async function renderTooltipAnchor({
jobs,

View file

@ -16,15 +16,15 @@ import React from 'react';
import {
ENVIRONMENT_ALL,
getEnvironmentLabel,
} from '../../../common/environment_filter_values';
import { getAPMHref } from '../../components/shared/Links/apm/APMLink';
import { useAnomalyDetectionJobsContext } from '../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { useLicenseContext } from '../../context/license/use_license_context';
import { useUrlParams } from '../../context/url_params_context/use_url_params';
import { FETCH_STATUS } from '../../hooks/use_fetcher';
import { APIReturnType } from '../../services/rest/createCallApmApi';
import { units } from '../../style/variables';
} from '../../../../common/environment_filter_values';
import { getAPMHref } from '../Links/apm/APMLink';
import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useLicenseContext } from '../../../context/license/use_license_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { units } from '../../../style/variables';
export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>;

View file

@ -9,13 +9,13 @@ import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useParams } from 'react-router-dom';
import { getAlertingCapabilities } from '../../components/alerting/get_alerting_capabilities';
import { getAPMHref } from '../../components/shared/Links/apm/APMLink';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { getAlertingCapabilities } from '../../alerting/get_alerting_capabilities';
import { getAPMHref } from '../Links/apm/APMLink';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link';
export function ActionMenu() {
export function ApmHeaderActionMenu() {
const { core, plugins } = useApmPluginContext();
const { serviceName } = useParams<{ serviceName?: string }>();
const { search } = window.location;

View file

@ -1,24 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiTabs } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
// Since our `EuiTab` components have `APMLink`s inside of them and not just
// `href`s, we need to override the color of the links inside or they will all
// be the primary color.
const StyledTabs = euiStyled(EuiTabs)`
padding: ${({ theme }) => `${theme.eui.gutterTypes.gutterMedium}`};
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
border-top: ${({ theme }) => theme.eui.euiBorderThin};
background: ${({ theme }) => theme.eui.euiColorEmptyShade};
`;
export function MainTabs({ children }: { children: ReactNode }) {
return <StyledTabs display="condensed">{children}</StyledTabs>;
}

View file

@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getByTestId, fireEvent, getByText } from '@testing-library/react';
import { createMemoryHistory, MemoryHistory } from 'history';
import React from 'react';
import { Router } from 'react-router-dom';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import { MockApmPluginContextWrapper } from '../../context/apm_plugin/mock_apm_plugin_context';
import { ApmServiceContextProvider } from '../../context/apm_service/apm_service_context';
import { UrlParamsProvider } from '../../context/url_params_context/url_params_context';
import { IUrlParams } from '../../context/url_params_context/types';
import * as useFetcherHook from '../../hooks/use_fetcher';
import * as useServiceTransactionTypesHook from '../../context/apm_service/use_service_transaction_types_fetcher';
import { renderWithTheme } from '../../utils/testHelpers';
import { fromQuery } from './Links/url_helpers';
import { CoreStart } from 'kibana/public';
import { SearchBar } from './search_bar';
function setup({
urlParams,
serviceTransactionTypes,
history,
}: {
urlParams: IUrlParams;
serviceTransactionTypes: string[];
history: MemoryHistory;
}) {
history.replace({
pathname: '/services/foo/transactions',
search: fromQuery(urlParams),
});
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiCounter: () => {} },
} as Partial<CoreStart>);
// mock transaction types
jest
.spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypesFetcher')
.mockReturnValue(serviceTransactionTypes);
jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any);
return renderWithTheme(
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper>
<Router history={history}>
<UrlParamsProvider>
<ApmServiceContextProvider>
<SearchBar showTransactionTypeSelector />
</ApmServiceContextProvider>
</UrlParamsProvider>
</Router>
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
);
}
describe('when transactionType is selected and multiple transaction types are given', () => {
let history: MemoryHistory;
beforeEach(() => {
history = createMemoryHistory();
jest.spyOn(history, 'push');
jest.spyOn(history, 'replace');
});
it('renders a radio group with transaction types', () => {
const { container } = setup({
history,
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
},
});
// transaction type selector
const dropdown = getByTestId(container, 'headerFilterTransactionType');
// both options should be listed
expect(getByText(dropdown, 'firstType')).toBeInTheDocument();
expect(getByText(dropdown, 'secondType')).toBeInTheDocument();
// second option should be selected
expect(dropdown).toHaveValue('secondType');
});
it('should update the URL when a transaction type is selected', () => {
const { container } = setup({
history,
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
},
});
expect(history.location.search).toEqual(
'?transactionType=secondType&rangeFrom=now-15m&rangeTo=now'
);
// transaction type selector
const dropdown = getByTestId(container, 'headerFilterTransactionType');
expect(getByText(dropdown, 'firstType')).toBeInTheDocument();
expect(getByText(dropdown, 'secondType')).toBeInTheDocument();
// change dropdown value
fireEvent.change(dropdown, { target: { value: 'firstType' } });
// assert that value was changed
expect(dropdown).toHaveValue('firstType');
expect(history.push).toHaveBeenCalled();
expect(history.location.search).toEqual(
'?transactionType=firstType&rangeFrom=now-15m&rangeTo=now'
);
});
});

View file

@ -15,7 +15,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
import { enableInspectEsQueries } from '../../../../observability/public';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { useKibanaUrl } from '../../hooks/useKibanaUrl';
@ -26,12 +25,9 @@ import { KueryBar } from './KueryBar';
import { TimeComparison } from './time_comparison';
import { TransactionTypeSelect } from './transaction_type_select';
const EuiFlexGroupSpaced = euiStyled(EuiFlexGroup)`
margin: ${({ theme }) =>
`${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`};
`;
interface Props {
hidden?: boolean;
showKueryBar?: boolean;
showTimeComparison?: boolean;
showTransactionTypeSelector?: boolean;
}
@ -49,7 +45,7 @@ function DebugQueryCallout() {
}
return (
<EuiFlexGroupSpaced>
<EuiFlexGroup>
<EuiFlexItem>
<EuiCallOut
title={i18n.translate(
@ -78,19 +74,26 @@ function DebugQueryCallout() {
/>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroupSpaced>
</EuiFlexGroup>
);
}
export function SearchBar({
hidden = false,
showKueryBar = true,
showTimeComparison = false,
showTransactionTypeSelector = false,
}: Props) {
const { isSmall, isMedium, isLarge, isXl, isXXL } = useBreakPoints();
if (hidden) {
return null;
}
return (
<>
<DebugQueryCallout />
<EuiFlexGroupSpaced
<EuiFlexGroup
gutterSize="s"
responsive={false}
direction={isXXL ? 'row' : 'column'}
@ -106,9 +109,12 @@ export function SearchBar({
<TransactionTypeSelect />
</EuiFlexItem>
)}
<EuiFlexItem>
<KueryBar />
</EuiFlexItem>
{showKueryBar && (
<EuiFlexItem>
<KueryBar />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={showTimeComparison && !isXXL}>
@ -128,7 +134,7 @@ export function SearchBar({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroupSpaced>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
);

View file

@ -14,12 +14,12 @@ import {
RULE_ID,
RULE_NAME,
} from '@kbn/rule-data-utils/target/technical_field_names';
import { parseTechnicalFields } from '../../../../../../rule_registry/common';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { asPercent, asDuration } from '../../../../../common/utils/formatters';
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
import { parseTechnicalFields } from '../../../../../rule_registry/common';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { asPercent, asDuration } from '../../../../common/utils/formatters';
import { TimestampTooltip } from '../TimestampTooltip';
interface AlertDetailProps {
alerts: APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts'];

View file

@ -9,7 +9,7 @@ import { EuiBadge, EuiDescriptionList } from '@elastic/eui';
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
type ServiceDetailsReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/metadata/details'>;

View file

@ -9,8 +9,8 @@ import { EuiDescriptionList } from '@elastic/eui';
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { asInteger } from '../../../../../common/utils/formatters';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { asInteger } from '../../../../common/utils/formatters';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
type ServiceDetailsReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/metadata/details'>;

View file

@ -13,8 +13,8 @@ import {
EuiPopoverTitle,
} from '@elastic/eui';
import React from 'react';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { px } from '../../../../style/variables';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { px } from '../../../style/variables';
interface IconPopoverProps {
title: string;

View file

@ -11,14 +11,14 @@ import { merge } from 'lodash';
// import { renderWithTheme } from '../../../../utils/testHelpers';
import React, { ReactNode } from 'react';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider';
import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../../context/apm_plugin/mock_apm_plugin_context';
import * as fetcherHook from '../../../../hooks/use_fetcher';
import { ServiceIcons } from './';
} from '../../../context/apm_plugin/mock_apm_plugin_context';
import * as fetcherHook from '../../../hooks/use_fetcher';
import { ServiceIcons } from '.';
import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
const KibanaReactContext = createKibanaReactContext({

View file

@ -8,12 +8,12 @@
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { ReactChild, useState } from 'react';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useTheme } from '../../../../hooks/use_theme';
import { ContainerType } from '../../../../../common/service_metadata';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { getAgentIcon } from '../../../shared/AgentIcon/get_agent_icon';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useTheme } from '../../../hooks/use_theme';
import { ContainerType } from '../../../../common/service_metadata';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { getAgentIcon } from '../AgentIcon/get_agent_icon';
import { CloudDetails } from './cloud_details';
import { ContainerDetails } from './container_details';
import { IconPopover } from './icon_popover';

View file

@ -9,7 +9,7 @@ import { EuiDescriptionList } from '@elastic/eui';
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
type ServiceDetailsReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/metadata/details'>;

View file

@ -9,7 +9,7 @@ import { renderHook } from '@testing-library/react-hooks';
import produce from 'immer';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { routes } from '../components/app/Main/route_config';
import { apmRouteConfig } from '../components/routing/apm_route_config';
import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
@ -36,7 +36,9 @@ function createWrapper(path: string) {
}
function mountBreadcrumb(path: string) {
renderHook(() => useBreadcrumbs(routes), { wrapper: createWrapper(path) });
renderHook(() => useBreadcrumbs(apmRouteConfig), {
wrapper: createWrapper(path),
});
}
const changeTitle = jest.fn();

View file

@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { of } from 'rxjs';
import type { ConfigSchema } from '.';
import {
AppMountParameters,
@ -34,6 +35,7 @@ import type {
FetchDataParams,
HasDataParams,
ObservabilityPublicSetup,
ObservabilityPublicStart,
} from '../../observability/public';
import type {
TriggersAndActionsUIPublicPluginSetup,
@ -48,24 +50,25 @@ export type ApmPluginStart = void;
export interface ApmPluginSetupDeps {
alerting?: AlertingPluginPublicSetup;
ml?: MlPluginSetup;
data: DataPublicPluginSetup;
features: FeaturesPluginSetup;
home?: HomePublicPluginSetup;
licensing: LicensingPluginSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
ml?: MlPluginSetup;
observability: ObservabilityPublicSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
}
export interface ApmPluginStartDeps {
alerting?: AlertingPluginPublicStart;
ml?: MlPluginStart;
data: DataPublicPluginStart;
embeddable: EmbeddableStart;
home: void;
licensing: void;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
ml?: MlPluginStart;
observability: ObservabilityPublicStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
@ -83,6 +86,21 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry);
}
// register observability nav
plugins.observability.navigation.registerSections(
of([
{
label: 'APM',
sortKey: 200,
entries: [
{ label: 'Services', app: 'apm', path: '/services' },
{ label: 'Traces', app: 'apm', path: '/traces' },
{ label: 'Service Map', app: 'apm', path: '/service-map' },
],
},
])
);
const getApmDataHelper = async () => {
const {
fetchObservabilityOverviewPageData,

Some files were not shown because too many files have changed in this diff Show more