[8.18] [APM][OTel] Encode service name in the APM URLs (#217092) (#218477)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[APM][OTel] Encode service name in the APM URLs
(#217092)](https://github.com/elastic/kibana/pull/217092)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT
[{"author":{"name":"jennypavlova","email":"dzheni.pavlova@elastic.co"},"sourceCommit":{"committedDate":"2025-04-16T15:52:33Z","message":"[APM][OTel]
Encode service name in the APM URLs (#217092)\n\nCloses #213943\n\n##
Summary\n\nThis PR ensures the service name is always encoded in the APM
UIs. It's\na follow-up of https://github.com/elastic/kibana/pull/215031
and aims to\nfind a better solution to the problem:\n- Add the encoding
directly to `formatRequest` as suggested there\n- I saw that there are
many places where we use legacy Url builders, so\nI will try to replace
them where possible and use\napm router link method where the path is
encoded\n([ref](7158e0201b/src/platform/packages/shared/kbn-typed-react-router-config/src/create_router.ts (L184-L185)))\n-
The PR includes the changes to address the issue above:\n - Replaced and
removed `LegacyAPMLink`\n- Refactored `useAPMHref` to support encoding
(and extracted and test\nthe encoding logic)\n - Example usage: \n -
Before: \n ```js\n useAPMHref({\n path:
`/services/${serviceName}/.....`,\n persistedFilters,\n });\n ```\n -
After:\n ```js\n useAPMHref({\n path:
'/services/{serviceName}/.......}',\n pathParams: { serviceName },\n
persistedFilters,\n });\n ```\n - Used the APM router link method as
much as possible\n\n\n## Testing\n- Run `node scripts/synthtrace
trace_with_service_names_with_slashes.ts\n--clean --live --uniqueIds
--live`\n- Go to service inventory and click the
links:\n\n\nhttps://github.com/user-attachments/assets/fcd4fbfc-4125-4cc8-9b00-53c5f375423f","sha":"f816e7b84f94d9af8d3fffb85dc83512f31c55e9","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","v9.0.0","backport:prev-minor","Team:obs-ux-infra_services","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[APM][OTel]
Encode service name in the APM
URLs","number":217092,"url":"https://github.com/elastic/kibana/pull/217092","mergeCommit":{"message":"[APM][OTel]
Encode service name in the APM URLs (#217092)\n\nCloses #213943\n\n##
Summary\n\nThis PR ensures the service name is always encoded in the APM
UIs. It's\na follow-up of https://github.com/elastic/kibana/pull/215031
and aims to\nfind a better solution to the problem:\n- Add the encoding
directly to `formatRequest` as suggested there\n- I saw that there are
many places where we use legacy Url builders, so\nI will try to replace
them where possible and use\napm router link method where the path is
encoded\n([ref](7158e0201b/src/platform/packages/shared/kbn-typed-react-router-config/src/create_router.ts (L184-L185)))\n-
The PR includes the changes to address the issue above:\n - Replaced and
removed `LegacyAPMLink`\n- Refactored `useAPMHref` to support encoding
(and extracted and test\nthe encoding logic)\n - Example usage: \n -
Before: \n ```js\n useAPMHref({\n path:
`/services/${serviceName}/.....`,\n persistedFilters,\n });\n ```\n -
After:\n ```js\n useAPMHref({\n path:
'/services/{serviceName}/.......}',\n pathParams: { serviceName },\n
persistedFilters,\n });\n ```\n - Used the APM router link method as
much as possible\n\n\n## Testing\n- Run `node scripts/synthtrace
trace_with_service_names_with_slashes.ts\n--clean --live --uniqueIds
--live`\n- Go to service inventory and click the
links:\n\n\nhttps://github.com/user-attachments/assets/fcd4fbfc-4125-4cc8-9b00-53c5f375423f","sha":"f816e7b84f94d9af8d3fffb85dc83512f31c55e9"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217092","number":217092,"mergeCommit":{"message":"[APM][OTel]
Encode service name in the APM URLs (#217092)\n\nCloses #213943\n\n##
Summary\n\nThis PR ensures the service name is always encoded in the APM
UIs. It's\na follow-up of https://github.com/elastic/kibana/pull/215031
and aims to\nfind a better solution to the problem:\n- Add the encoding
directly to `formatRequest` as suggested there\n- I saw that there are
many places where we use legacy Url builders, so\nI will try to replace
them where possible and use\napm router link method where the path is
encoded\n([ref](7158e0201b/src/platform/packages/shared/kbn-typed-react-router-config/src/create_router.ts (L184-L185)))\n-
The PR includes the changes to address the issue above:\n - Replaced and
removed `LegacyAPMLink`\n- Refactored `useAPMHref` to support encoding
(and extracted and test\nthe encoding logic)\n - Example usage: \n -
Before: \n ```js\n useAPMHref({\n path:
`/services/${serviceName}/.....`,\n persistedFilters,\n });\n ```\n -
After:\n ```js\n useAPMHref({\n path:
'/services/{serviceName}/.......}',\n pathParams: { serviceName },\n
persistedFilters,\n });\n ```\n - Used the APM router link method as
much as possible\n\n\n## Testing\n- Run `node scripts/synthtrace
trace_with_service_names_with_slashes.ts\n--clean --live --uniqueIds
--live`\n- Go to service inventory and click the
links:\n\n\nhttps://github.com/user-attachments/assets/fcd4fbfc-4125-4cc8-9b00-53c5f375423f","sha":"f816e7b84f94d9af8d3fffb85dc83512f31c55e9"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
jennypavlova 2025-04-16 20:53:14 +02:00 committed by GitHub
parent fde4a3d9ea
commit 906ff07331
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 661 additions and 382 deletions

View file

@ -0,0 +1,105 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ApmFields, apm, Instance } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
import { withClient } from '../lib/utils/with_client';
const ENVIRONMENT = getSynthtraceEnvironment(__filename);
const scenario: Scenario<ApmFields> = async (runOptions) => {
const { logger } = runOptions;
const { numServices = 3 } = runOptions.scenarioOpts || {};
return {
generate: ({ range, clients: { apmEsClient } }) => {
const transactionName = '240rpm/75% 1000ms';
const successfulTimestamps = range.interval('1m').rate(180);
const failedTimestamps = range.interval('1m').rate(180);
const instances = [...Array(numServices).keys()].map((index) =>
apm
.service({ name: `synth/node-${index}`, environment: ENVIRONMENT, agentName: 'nodejs' })
.instance('instance')
);
const instanceSpans = (instance: Instance) => {
const successfulTraceEvents = successfulTimestamps.generator((timestamp) =>
instance
.transaction({ transactionName })
.timestamp(timestamp)
.defaults({
'url.domain': 'foo.bar',
})
.duration(1000)
.success()
.children(
instance
.span({
spanName: 'GET apm-*/_search',
spanType: 'db',
spanSubtype: 'elasticsearch',
})
.duration(1000)
.success()
.destination('elasticsearch')
.timestamp(timestamp),
instance
.span({ spanName: 'custom_operation', spanType: 'custom' })
.duration(100)
.success()
.timestamp(timestamp)
)
);
const failedTraceEvents = failedTimestamps.generator((timestamp) =>
instance
.transaction({ transactionName })
.timestamp(timestamp)
.duration(1000)
.failure()
.errors(
instance
.error({
message: '[ResponseError] index_not_found_exception',
type: 'ResponseError',
})
.timestamp(timestamp + 50)
)
);
const metricsets = range
.interval('30s')
.rate(1)
.generator((timestamp) =>
instance
.appMetrics({
'system.memory.actual.free': 800,
'system.memory.total': 1000,
'system.cpu.total.norm.pct': 0.6,
'system.process.cpu.total.norm.pct': 0.7,
})
.timestamp(timestamp)
);
return [successfulTraceEvents, failedTraceEvents, metricsets];
};
return withClient(
apmEsClient,
logger.perf('generating_apm_events', () =>
instances.flatMap((instance) => instanceSpans(instance))
)
);
},
};
};
export default scenario;

View file

@ -11,6 +11,15 @@ import { formatRequest } from './format_request';
describe('formatRequest', () => {
const version = 1;
it('should encode the path if the optional or required param is provided', () => {
const pathParams = { param: 'test/Param/>?%/' };
const resultOptionalEnd = formatRequest(`GET /api/endpoint/{param?} ${version}`, pathParams);
expect(resultOptionalEnd.pathname).toBe('/api/endpoint/test%2FParam%2F%3E%3F%25%2F');
const resultRequiredEnd = formatRequest(`GET /api/endpoint/{param} ${version}`, pathParams);
expect(resultRequiredEnd.pathname).toBe('/api/endpoint/test%2FParam%2F%3E%3F%25%2F');
const resultRequiredMid = formatRequest(`GET /api/{param}/endpoint/ ${version}`, pathParams);
expect(resultRequiredMid.pathname).toBe('/api/test%2FParam%2F%3E%3F%25%2F/endpoint/');
});
it('should return the correct path if the optional or required param is provided', () => {
const pathParams = { param: 'testParam' };
const resultOptionalEnd = formatRequest(`GET /api/endpoint/{param?} ${version}`, pathParams);

View file

@ -21,8 +21,8 @@ export function formatRequest(endpoint: string, pathParams: Record<string, any>
const pathname = Object.keys(pathParams).reduce((acc, paramName) => {
return acc
.replace(`{${paramName}}`, pathParams[paramName])
.replace(`{${paramName}?}`, pathParams[paramName]);
.replace(`{${paramName}}`, encodeURIComponent(pathParams[paramName]))
.replace(`{${paramName}?}`, encodeURIComponent(pathParams[paramName]));
}, rawPathname);
if ((pathname.match(optionalReg) ?? [])?.length > 0) {

View file

@ -8,6 +8,7 @@
*/
export * from './src/create_router';
export * from './src/encode_path';
export * from './src/types';
export * from './src/outlet';
export * from './src/route_renderer';

View file

@ -19,6 +19,7 @@ import {
RouteConfig as ReactRouterConfig,
} from 'react-router-config';
import { FlattenRoutesOf, Route, RouteMap, Router, RouteWithPath } from './types';
import { encodePath } from './encode_path';
function toReactRouterPath(path: string) {
return path.replace(/(?:{([^\/]+)})/g, ':$1');
@ -177,13 +178,7 @@ export function createRouter<TRoutes extends RouteMap>(routes: TRoutes): Router<
const paramsWithBuiltInDefaults = merge({ path: {}, query: {} }, params);
path = path
.split('/')
.map((part) => {
const match = part.match(/(?:{([a-zA-Z]+)})/);
return match ? encodeURIComponent(paramsWithBuiltInDefaults.path[match[1]]) : part;
})
.join('/');
path = encodePath(path, paramsWithBuiltInDefaults?.path);
const matchedRoutes = getRoutesToMatch(path);

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { encodePath } from './encode_path';
describe('encodePath', () => {
it('should return the same path if no pathParams are provided', () => {
const path = '/services/{serviceName}/transactions';
const result = encodePath(path);
expect(result).toBe(path);
});
it('should encode path parameters correctly', () => {
const path = '/services/{serviceName}/transactions';
const pathParams = { serviceName: 'my/service' };
const result = encodePath(path, pathParams);
expect(result).toBe('/services/my%2Fservice/transactions');
});
it('should handle two matching path parameters', () => {
const path = '/services/{serviceName}/transactions/{transactionId}';
const pathParams = { serviceName: 'my/service', transactionId: '123/456' };
const result = encodePath(path, pathParams);
expect(result).toBe('/services/my%2Fservice/transactions/123%2F456');
});
it('should handle multiple path parameters', () => {
const path = '/services/{serviceName}/transactions/{transactionId}/details/{detailId}';
const pathParams = {
serviceName: 'my/service',
transactionId: '123/456',
detailId: '111/222/333',
};
const result = encodePath(path, pathParams);
expect(result).toBe('/services/my%2Fservice/transactions/123%2F456/details/111%2F222%2F333');
});
it('should return the same path if no matching parameters are found', () => {
const path = '/services/{serviceName}/transactions';
const pathParams = { otherParam: 'value' };
const result = encodePath(path, pathParams);
expect(result).toBe(path);
});
it('should handle a path without placeholders', () => {
const path = '/services/transactions';
const pathParams = { serviceName: 'my/service' };
const result = encodePath(path, pathParams);
expect(result).toBe('/services/transactions');
});
});

View file

@ -0,0 +1,19 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export const encodePath = (path: string, pathParams?: Record<string, string>) =>
pathParams && Object.keys(pathParams).length > 0
? path
.split('/')
.map((part) => {
const match = part.match(/(?:{([a-zA-Z]+)})/);
return match && pathParams[match[1]] ? encodeURIComponent(pathParams[match[1]]) : part;
})
.join('/')
: path;

View file

@ -11,6 +11,7 @@ import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED } from './environment_filter_v
export const environmentStringRt = t.union([
t.literal(ENVIRONMENT_NOT_DEFINED.value),
t.literal(ENVIRONMENT_ALL.value),
t.string,
nonEmptyStringRt,
]);

View file

@ -72,7 +72,7 @@ const serviceOverviewLink = apmRouter.link('/services/:serviceName', {
If you're not in React context, you can also import `apmRouter` directly and call its `link` function - but you have to prepend the basePath manually in that case.
We also have the [`getLegacyApmHref` function and `LegacyAPMLink` component](../public/components/shared/links/apm/apm_link.tsx), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
We also have the [`getLegacyApmHref` and `useAPMHref` functions](../public/components/shared/links/apm/apm_link_hooks.ts), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
### Cross-app linking

View file

@ -56,6 +56,7 @@ import { ErrorTabKey, getTabs } from './error_tabs';
import { ErrorUiActionsContextMenu } from './error_ui_actions_context_menu';
import { SampleSummary } from './sample_summary';
import { ErrorSampleContextualInsight } from './error_sample_contextual_insight';
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
const TransactionLinkName = euiStyled.div`
margin-left: ${({ theme }) => theme.eui.euiSizeS};
@ -91,7 +92,7 @@ export function ErrorSampleDetails({
urlParams: { detailTab, offset, comparisonEnabled },
} = useLegacyUrlParams();
const { uiActions } = useApmPluginContext();
const { uiActions, core } = useApmPluginContext();
const router = useApmRouter();
@ -114,6 +115,11 @@ export function ErrorSampleDetails({
const isSucceeded = isSuccess(errorSamplesFetchStatus) && isSuccess(errorFetchStatus);
const defaultComparisonEnabled = getComparisonEnabled({
core,
urlComparisonEnabled: comparisonEnabled,
});
useEffect(() => {
setSampleActivePage(0);
}, [errorSampleIds]);
@ -258,13 +264,21 @@ export function ErrorSampleDetails({
})}
>
<TransactionDetailLink
traceId={transaction.trace.id}
transactionId={transaction.transaction.id}
transactionName={transaction.transaction.name}
transactionType={transaction.transaction.type}
serviceName={transaction.service.name}
offset={offset}
comparisonEnabled={comparisonEnabled}
href={router.link('/services/{serviceName}/transactions/view', {
path: { serviceName: transaction.service.name },
query: {
...query,
traceId: transaction.trace.id,
transactionId: transaction.transaction.id,
transactionName: transaction.transaction.name,
transactionType: transaction.transaction.type,
comparisonEnabled: defaultComparisonEnabled,
showCriticalPath: false,
offset,
kuery,
},
})}
>
<EuiIcon type="merge" />
<TransactionLinkName>{transaction.transaction.name}</TransactionLinkName>

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiBasicTable, EuiTitle, RIGHT_ALIGNMENT, EuiSpacer } from '@elastic/eui';
import type { ValuesType } from 'utility-types';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import type { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { SparkPlot } from '../../../shared/charts/spark_plot';
import { ChartType, getTimeSeriesColor } from '../../../shared/charts/helper/get_timeseries_color';
@ -37,6 +38,7 @@ export function TopErroneousTransactions({ serviceName }: Props) {
query,
path: { groupId },
} = useApmParams('/services/{serviceName}/errors/{groupId}');
const { link } = useApmRouter();
const { rangeFrom, rangeTo, environment, kuery, offset, comparisonEnabled } = query;
@ -86,11 +88,18 @@ export function TopErroneousTransactions({ serviceName }: Props) {
text={transactionName}
content={
<TransactionDetailLink
serviceName={serviceName}
transactionName={transactionName}
transactionType={transactionType ?? ''}
comparisonEnabled={comparisonEnabled}
offset={offset}
href={link('/services/{serviceName}/transactions/view', {
path: { serviceName },
query: {
...query,
transactionName,
transactionType: transactionType ?? '',
comparisonEnabled,
showCriticalPath: false,
offset,
},
})}
>
{transactionName}
</TransactionDetailLink>

View file

@ -129,6 +129,7 @@ export function ErrorGroupList({
<GroupIdLink
serviceName={serviceName}
errorGroupId={groupId}
query={query}
data-test-subj="errorGroupId"
>
{groupId.slice(0, 5) || NOT_AVAILABLE_LABEL}
@ -172,7 +173,7 @@ export function ErrorGroupList({
return (
<MessageAndCulpritCell>
<EuiToolTip id="error-message-tooltip" content={item.name || NOT_AVAILABLE_LABEL}>
<MessageLink serviceName={serviceName} errorGroupId={item.groupId}>
<MessageLink serviceName={serviceName} errorGroupId={item.groupId} query={query}>
{item.name || NOT_AVAILABLE_LABEL}
</MessageLink>
</EuiToolTip>

View file

@ -11,7 +11,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useTheme } from '../../../hooks/use_theme';
import { getLegacyApmHref } from '../../shared/links/apm/apm_link';
import { getLegacyApmHref } from '../../shared/links/apm/apm_link_hooks';
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
import type { APMQueryParams } from '../../shared/links/url_helpers';
import { CytoscapeContext } from './cytoscape';

View file

@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import type { ReactNode } from 'react';
import React from 'react';
import { ActionMenu } from '@kbn/observability-shared-plugin/public';
import type { TypeOf } from '@kbn/typed-react-router-config';
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
import type { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import { getServiceNodeName, SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes';
@ -26,6 +27,7 @@ import { getLatencyColumnLabel } from '../../../shared/transactions_table/get_la
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
import { InstanceActionsMenu } from './instance_actions_menu';
import { ChartType, getTimeSeriesColor } from '../../../shared/charts/helper/get_timeseries_color';
import type { ApmRoutes } from '../../../routing/apm_route_config';
type ServiceInstanceMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics'>;
@ -46,6 +48,7 @@ export function getColumns({
itemIdToOpenActionMenuRowMap,
offset,
shouldShowSparkPlots = true,
query,
}: {
serviceName: string;
kuery: string;
@ -59,6 +62,7 @@ export function getColumns({
toggleRowActionMenu: (selectedServiceNodeName: string) => void;
itemIdToOpenActionMenuRowMap: Record<string, boolean>;
shouldShowSparkPlots?: boolean;
query: Omit<TypeOf<ApmRoutes, '/services/{serviceName}/metrics'>['query'], 'kuery'>;
}): Array<EuiBasicTableColumn<MainStatsServiceInstanceItem>> {
return [
{
@ -75,12 +79,12 @@ export function getColumns({
const link = (
<MetricOverviewLink
serviceName={serviceName}
mergeQuery={(query) => ({
query={{
...query,
kuery: isMissingServiceNodeName
? `NOT (service.node.name:*)`
: `service.node.name:"${item.serviceNodeName}"`,
})}
}}
>
{text}
</MetricOverviewLink>

View file

@ -63,6 +63,7 @@ export function ServiceOverviewInstancesTable({
isNotInitiated,
}: Props) {
const {
query,
query: { kuery, latencyAggregationType, comparisonEnabled, offset },
} = useApmParams('/services/{serviceName}');
@ -125,6 +126,7 @@ export function ServiceOverviewInstancesTable({
itemIdToOpenActionMenuRowMap,
shouldShowSparkPlots,
offset,
query,
});
const pagination = {

View file

@ -5,11 +5,12 @@
* 2.0.
*/
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton, EuiCallOut } from '@elastic/eui';
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton, EuiCallOut, EuiLink } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { isString } from 'lodash';
import { EuiButtonEmpty } from '@elastic/eui';
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
import type { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types';
import {
omitAllOption,
@ -18,7 +19,6 @@ import {
} from '../../../../../../../common/agent_configuration/all_option';
import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/use_fetcher';
import { FormRowSelect } from './form_row_select';
import { LegacyAPMLink } from '../../../../../shared/links/apm/apm_link';
import { FormRowSuggestionsSelect } from './form_row_suggestions_select';
import { SERVICE_NAME } from '../../../../../../../common/es_fields/apm';
import { isOpenTelemetryAgentName } from '../../../../../../../common/agent_name';
@ -45,6 +45,8 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
{ preservePreviousData: false }
);
const { link } = useApmRouter();
const environments = environmentsData?.environments ?? [];
const { status: agentNameStatus } = useFetcher(
@ -160,13 +162,22 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
<EuiFlexGroup justifyContent="flexEnd">
{/* Cancel button */}
<EuiFlexItem grow={false}>
<LegacyAPMLink path="/settings/agent-configuration">
<EuiButtonEmpty data-test-subj="apmServicePageCancelButton" color="primary">
<EuiLink
data-test-subj="apmAgentConfigurationCancelButton"
href={link('/settings/agent-configuration')}
>
<EuiButtonEmpty
aria-label={i18n.translate('xpack.apm.servicePage.cancelButton.ariaLabel', {
defaultMessage: 'Cancel',
})}
data-test-subj="apmServicePageCancelButton"
color="primary"
>
{i18n.translate('xpack.apm.agentConfig.servicePage.cancelButton', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</LegacyAPMLink>
</EuiLink>
</EuiFlexItem>
{/* Next button */}

View file

@ -11,6 +11,11 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import type { ValuesType } from 'utility-types';
import type { TypeOf } from '@kbn/typed-react-router-config';
import { getComparisonEnabled } from '../../../../../shared/time_comparison/get_comparison_enabled';
import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../../../common/environment_filter_values';
import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params';
import { MetricOverviewLink } from '../../../../../shared/links/apm/metric_overview_link';
import { AgentExplorerFieldName } from '../../../../../../../common/agent_explorer';
import { isOpenTelemetryAgentName } from '../../../../../../../common/agent_name';
@ -26,6 +31,7 @@ import { ItemsBadge } from '../../../../../shared/item_badge';
import { PopoverTooltip } from '../../../../../shared/popover_tooltip';
import { TimestampTooltip } from '../../../../../shared/timestamp_tooltip';
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
import type { ApmRoutes } from '../../../../../routing/apm_route_config';
type AgentExplorerInstance = ValuesType<
APIReturnType<'GET /internal/apm/services/{serviceName}/agent_instances'>['items']
@ -38,11 +44,19 @@ enum AgentExplorerInstanceFieldName {
LastReport = 'lastReport',
}
export function getInstanceColumns(
serviceName: string,
agentName: AgentName,
agentDocsPageUrl?: string
): Array<EuiBasicTableColumn<AgentExplorerInstance>> {
interface GetInstanceColumnsProps {
serviceName: string;
agentName: AgentName;
query: Omit<TypeOf<ApmRoutes, '/services/{serviceName}/metrics'>['query'], 'kuery'>;
agentDocsPageUrl?: string;
}
export function getInstanceColumns({
serviceName,
agentName,
query,
agentDocsPageUrl,
}: GetInstanceColumnsProps): Array<EuiBasicTableColumn<AgentExplorerInstance>> {
return [
{
field: AgentExplorerInstanceFieldName.InstanceName,
@ -64,7 +78,7 @@ export function getInstanceColumns(
}
)}
>
<EuiText style={{ width: `${unit * 24}px` }} size="s">
<EuiText css={{ width: `${unit * 24}px` }} size="s">
<p>
<FormattedMessage
defaultMessage="You can configure the service node name through {seeDocs}."
@ -103,10 +117,10 @@ export function getInstanceColumns(
{serviceNode ? (
<MetricOverviewLink
serviceName={serviceName}
mergeQuery={(query) => ({
query={{
...query,
kuery: `service.node.name:"${displayedName}"`,
})}
}}
>
{displayedName}
</MetricOverviewLink>
@ -178,11 +192,34 @@ export function AgentInstancesDetails({
items,
isLoading,
}: Props) {
const {
query,
query: { environment, rangeFrom, rangeTo, serviceGroup },
} = useAnyOfApmParams('/services/{serviceName}/overview', '/services/{serviceName}/metrics');
const { core } = useApmPluginContext();
const defaultComparisonEnabled = getComparisonEnabled({
core,
urlComparisonEnabled: query.comparisonEnabled,
});
return (
<>
<EuiInMemoryTable
items={items}
columns={getInstanceColumns(serviceName, agentName, agentDocsPageUrl)}
columns={getInstanceColumns({
serviceName,
agentName,
query: {
...query,
environment: environment ?? ENVIRONMENT_NOT_DEFINED.value,
rangeFrom,
rangeTo,
serviceGroup,
comparisonEnabled: defaultComparisonEnabled,
},
agentDocsPageUrl,
})}
pagination={true}
sorting={{
sort: {

View file

@ -9,7 +9,7 @@ import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { LegacyAPMLink } from '../../../../shared/links/apm/apm_link';
import { HomeLink } from '../../../../shared/links/apm/home_link';
import { useFleetCloudAgentPolicyHref } from '../../../../shared/links/kibana';
export function CardFooterContent() {
@ -33,12 +33,12 @@ export function CardFooterContent() {
defaultMessage="or simply return to the {serviceInventoryLink}."
values={{
serviceInventoryLink: (
<LegacyAPMLink path="/services">
<HomeLink>
{i18n.translate(
'xpack.apm.settings.schema.success.returnText.serviceInventoryLink',
{ defaultMessage: 'Service inventory' }
)}
</LegacyAPMLink>
</HomeLink>
),
}}
/>

View file

@ -11,6 +11,7 @@ import type { TypeOf } from '@kbn/typed-react-router-config';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useMemo } from 'react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { useApmRouter } from '../../../hooks/use_apm_router';
import type { ApmRoutes } from '../../routing/apm_route_config';
import { asMillisecondDuration, asTransactionRate } from '../../../../common/utils/formatters';
import { useApmParams } from '../../../hooks/use_apm_params';
@ -40,8 +41,16 @@ type TraceGroup = Required<Props['response']>['data']['items'][number];
export function getTraceListColumns({
query,
link,
}: {
query: TypeOf<ApmRoutes, '/traces'>['query'];
link: (
path: '/services/{serviceName}/transactions/view',
params: {
path: { serviceName: string };
query: TypeOf<ApmRoutes, '/services/{serviceName}/transactions/view'>['query'];
}
) => string;
}): Array<ITableColumn<TraceGroup>> {
return [
{
@ -54,9 +63,17 @@ export function getTraceListColumns({
render: (_: string, { serviceName, transactionName, transactionType }: TraceGroup) => (
<EuiToolTip content={transactionName} anchorClassName="eui-textTruncate">
<StyledTransactionLink
serviceName={serviceName}
transactionName={transactionName}
transactionType={transactionType}
href={link('/services/{serviceName}/transactions/view', {
path: { serviceName },
query: {
...query,
transactionName,
transactionType,
serviceGroup: '',
showCriticalPath: false,
},
})}
>
{transactionName}
</StyledTransactionLink>
@ -141,8 +158,9 @@ export function TraceList({ response }: Props) {
query,
query: { rangeFrom, rangeTo },
} = useApmParams('/traces');
const { link } = useApmRouter();
const traceListColumns = useMemo(() => getTraceListColumns({ query }), [query]);
const traceListColumns = useMemo(() => getTraceListColumns({ query, link }), [query, link]);
useEffect(() => {
if (status === FETCH_STATUS.SUCCESS) {

View file

@ -8,6 +8,8 @@
import { EuiButton, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values';
import type { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/transaction';
import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link';
@ -15,6 +17,7 @@ import type { IWaterfall } from './waterfall_container/waterfall/waterfall_helpe
import type { Environment } from '../../../../../common/environment_rt';
import { useAnyOfApmParams } from '../../../../hooks/use_apm_params';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
function FullTraceButton({ isLoading, isDisabled }: { isLoading?: boolean; isDisabled?: boolean }) {
return (
@ -53,6 +56,14 @@ export function MaybeViewTraceLink({
'/dependencies/operation'
);
const { link } = useApmRouter();
const { core } = useApmPluginContext();
const defaultComparisonEnabled = getComparisonEnabled({
core,
urlComparisonEnabled: comparisonEnabled,
});
const latencyAggregationType =
('latencyAggregationType' in query && query.latencyAggregationType) ||
LatencyAggregationType.avg;
@ -77,6 +88,10 @@ export function MaybeViewTraceLink({
const rootTransaction = rootWaterfallTransaction.doc;
const isRoot = transaction.transaction.id === rootWaterfallTransaction.id;
const nextEnvironment = getNextEnvironmentUrlParam({
requestedEnvironment: rootTransaction.service.environment,
currentEnvironmentUrlParam: environment,
});
// the user is already viewing the full trace, so don't link to it
if (isRoot) {
@ -92,22 +107,24 @@ export function MaybeViewTraceLink({
// the user is viewing a zoomed in version of the trace. Link to the full trace
} else {
const nextEnvironment = getNextEnvironmentUrlParam({
requestedEnvironment: rootTransaction.service.environment,
currentEnvironmentUrlParam: environment,
});
return (
<TransactionDetailLink
serviceName={rootTransaction.service.name}
transactionId={rootTransaction.transaction.id}
traceId={rootTransaction.trace.id}
transactionName={rootTransaction.transaction.name}
transactionType={rootTransaction.transaction.type}
environment={nextEnvironment}
latencyAggregationType={latencyAggregationType}
comparisonEnabled={comparisonEnabled}
offset={offset}
href={link('/services/{serviceName}/transactions/view', {
path: { serviceName: rootTransaction.service.name },
query: {
...query,
latencyAggregationType,
traceId: rootTransaction.trace.id,
transactionId: rootTransaction.transaction.id,
transactionName: rootTransaction.transaction.name,
transactionType: rootTransaction.transaction.type,
comparisonEnabled: defaultComparisonEnabled,
offset,
environment: nextEnvironment,
serviceGroup: '',
},
})}
>
<FullTraceButton />
</TransactionDetailLink>

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
import { SERVICE_NAME, TRANSACTION_NAME } from '../../../../../../../common/es_fields/apm';
import { getNextEnvironmentUrlParam } from '../../../../../../../common/environment_filter_values';
import { LatencyAggregationType } from '../../../../../../../common/latency_aggregation_types';
@ -27,14 +28,13 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
'/traces/explorer',
'/dependencies/operation'
);
const { link } = useApmRouter();
const latencyAggregationType =
('latencyAggregationType' in query && query.latencyAggregationType) ||
LatencyAggregationType.avg;
const serviceGroup = ('serviceGroup' in query && query.serviceGroup) || '';
const { comparisonEnabled, offset } = query;
if (!transaction) {
return null;
}
@ -76,15 +76,16 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
fieldName: TRANSACTION_NAME,
val: (
<TransactionDetailLink
serviceName={transaction.service.name}
transactionId={transaction.transaction.id}
traceId={transaction.trace.id}
transactionName={transaction.transaction.name}
transactionType={transaction.transaction.type}
environment={nextEnvironment}
latencyAggregationType={latencyAggregationType}
comparisonEnabled={comparisonEnabled}
offset={offset}
href={link('/services/{serviceName}/transactions/view', {
path: { serviceName: transaction.service.name },
query: {
...query,
serviceGroup,
latencyAggregationType,
transactionName: transaction.transaction.name,
},
})}
>
{transaction.transaction.name}
</TransactionDetailLink>

View file

@ -8,6 +8,7 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { METRIC_TYPE, useUiTracker } from '@kbn/observability-shared-plugin/public';
import { useApmRouter } from '../../../../../../../hooks/use_apm_router';
import {
SERVICE_NAME,
SPAN_DESTINATION_SERVICE_RESOURCE,
@ -37,7 +38,9 @@ export function StickySpanProperties({ span, transaction }: Props) {
'/traces/explorer',
'/dependencies/operation'
);
const { environment, comparisonEnabled, offset } = query;
const router = useApmRouter();
const { environment } = query;
const latencyAggregationType =
('latencyAggregationType' in query && query.latencyAggregationType) ||
@ -82,15 +85,17 @@ export function StickySpanProperties({ span, transaction }: Props) {
fieldName: TRANSACTION_NAME,
val: (
<TransactionDetailLink
serviceName={transaction.service.name}
transactionId={transaction.transaction.id}
traceId={transaction.trace.id}
transactionName={transaction.transaction.name}
transactionType={transaction.transaction.type}
environment={nextEnvironment}
latencyAggregationType={latencyAggregationType}
comparisonEnabled={comparisonEnabled}
offset={offset}
href={router.link('/services/{serviceName}/transactions/view', {
path: { serviceName: transaction.service.name },
query: {
...query,
environment: nextEnvironment,
serviceGroup,
latencyAggregationType,
transactionName: transaction.transaction.name,
},
})}
>
{transaction.transaction.name}
</TransactionDetailLink>

View file

@ -19,7 +19,7 @@ import { useAnomalyDetectionJobsContext } from '../../../../context/anomaly_dete
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useTheme } from '../../../../hooks/use_theme';
import { getLegacyApmHref } from '../../../shared/links/apm/apm_link';
import { getLegacyApmHref } from '../../../shared/links/apm/apm_link_hooks';
export function AnomalyDetectionSetupLink() {
const { query } = useApmParams('/*');

View file

@ -10,7 +10,7 @@ import { apmLabsButton } from '@kbn/observability-plugin/common';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { getAlertingCapabilities } from '../../../alerting/utils/get_alerting_capabilities';
import { getLegacyApmHref } from '../../../shared/links/apm/apm_link';
import { getLegacyApmHref } from '../../../shared/links/apm/apm_link_hooks';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link';

View file

@ -16,7 +16,11 @@ import { ErrorMarker } from './error_marker';
function Wrapper({ children }: { children?: ReactNode }) {
return (
<MemoryRouter>
<MemoryRouter
initialEntries={[
'/services/{serviceName}/errors?comparisonEnabled=false&latencyAggregationType=avg&offset=1d&rangeFrom=now-15m&rangeTo=now&serviceGroup=&transactionType=request',
]}
>
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
</MemoryRouter>
);
@ -54,7 +58,9 @@ describe('ErrorMarker', () => {
return component;
}
function getKueryDecoded(url: string) {
return decodeURIComponent(url.substring(url.indexOf('kuery='), url.indexOf('&')));
return decodeURIComponent(
url.substring(url.indexOf('kuery='), url.indexOf('&latencyAggregationType'))
);
}
it('renders link with trace and transaction', () => {
const component = openPopover(mark);

View file

@ -10,8 +10,8 @@ import React, { useState } from 'react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { TRACE_ID, TRANSACTION_ID } from '../../../../../../common/es_fields/apm';
import { asDuration } from '../../../../../../common/utils/formatters';
import { useLegacyUrlParams } from '../../../../../context/url_params_context/use_url_params';
import { useTheme } from '../../../../../hooks/use_theme';
import { useAnyOfApmParams } from '../../../../../hooks/use_apm_params';
import type { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_error_marks';
import { ErrorDetailLink } from '../../../links/apm/error_detail_link';
import { Legend, Shape } from '../legend';
@ -52,8 +52,13 @@ function truncateMessage(errorMessage?: string) {
export function ErrorMarker({ mark }: Props) {
const theme = useTheme();
const { urlParams } = useLegacyUrlParams();
const [isPopoverOpen, showPopover] = useState(false);
const { query } = useAnyOfApmParams(
'/services/{serviceName}/overview',
'/services/{serviceName}/errors',
'/services/{serviceName}/transactions/view',
'/traces/explorer/waterfall'
);
const togglePopover = () => showPopover(!isPopoverOpen);
@ -68,16 +73,15 @@ export function ErrorMarker({ mark }: Props) {
);
const { error } = mark;
const serviceGroup = 'serviceGroup' in query ? query.serviceGroup : '';
const { rangeTo, rangeFrom } = urlParams;
const query = {
const queryParam = {
...query,
serviceGroup,
kuery: [
...(error.trace?.id ? [`${TRACE_ID} : "${error.trace?.id}"`] : []),
...(error.transaction?.id ? [`${TRANSACTION_ID} : "${error.transaction?.id}"`] : []),
].join(' and '),
rangeFrom,
rangeTo,
};
const errorMessage = error.error.log?.message || error.error.exception?.[0]?.message;
@ -107,7 +111,7 @@ export function ErrorMarker({ mark }: Props) {
data-test-subj="errorLink"
serviceName={error.service.name}
errorGroupId={error.error.grouping_key}
query={query}
query={queryParam}
title={errorMessage}
>
{truncatedErrorMessage}

View file

@ -88,7 +88,7 @@ export function getColumns({
<TruncateWithTooltip
text={name}
content={
<ErrorDetailLink serviceName={serviceName} errorGroupId={errorGroupId}>
<ErrorDetailLink serviceName={serviceName} errorGroupId={errorGroupId} query={query}>
{name}
</ErrorDetailLink>
}

View file

@ -7,7 +7,7 @@
import type { IBasePath } from '@kbn/core/public';
import type { AgentConfigurationIntake } from '../../../../../common/agent_configuration/configuration_types';
import { getLegacyApmHref } from './apm_link';
import { getLegacyApmHref } from './apm_link_hooks';
export function editAgentConfigurationHref(
configService: AgentConfigurationIntake['service'],

View file

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

View file

@ -6,12 +6,11 @@
*/
import type { EuiLinkAnchorProps } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import type { IBasePath } from '@kbn/core/public';
import { pick } from 'lodash';
import React from 'react';
import { useLocation } from 'react-router-dom';
import url from 'url';
import { encodePath } from '@kbn/typed-react-router-config';
import { pickKeys } from '../../../../../common/utils/pick_keys';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
@ -45,10 +44,12 @@ export function useAPMHref({
path,
persistedFilters,
query,
pathParams,
}: {
path: string;
persistedFilters?: Array<keyof APMQueryParams>;
query?: APMQueryParams;
pathParams?: Record<string, string>;
}) {
const { urlParams } = useLegacyUrlParams();
const { basePath } = useApmPluginContext().core.http;
@ -58,7 +59,9 @@ export function useAPMHref({
...query,
};
return getLegacyApmHref({ basePath, path, query: nextQuery, search });
const encodedPath = encodePath(path, pathParams);
return getLegacyApmHref({ basePath, path: encodedPath, query: nextQuery, search });
}
/**
@ -87,15 +90,3 @@ export function getLegacyApmHref({
search: nextSearch,
});
}
export function LegacyAPMLink({ path = '', query, mergeQuery, ...rest }: Props) {
const { core } = useApmPluginContext();
const { search } = useLocation();
const { basePath } = core.http;
const mergedQuery = mergeQuery ? mergeQuery(query ?? {}) : query;
const href = getLegacyApmHref({ basePath, path, search, query: mergedQuery });
return <EuiLink data-test-subj="apmLegacyAPMLinkLink" {...rest} href={href} />;
}

View file

@ -6,16 +6,28 @@
*/
import React from 'react';
import type { APMLinkExtendProps } from './apm_link';
import { LegacyAPMLink } from './apm_link';
import type { TypeOf } from '@kbn/typed-react-router-config';
import { EuiLink } from '@elastic/eui';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import type { APMLinkExtendProps } from './apm_link_hooks';
import type { ApmRoutes } from '../../../routing/apm_route_config';
interface Props extends APMLinkExtendProps {
serviceName: string;
errorGroupId: string;
query: TypeOf<ApmRoutes, '/services/{serviceName}/errors/{groupId}'>['query'];
}
function ErrorDetailLink({ serviceName, errorGroupId, ...rest }: Props) {
return <LegacyAPMLink path={`/services/${serviceName}/errors/${errorGroupId}`} {...rest} />;
function ErrorDetailLink({ serviceName, errorGroupId, query, ...rest }: Props) {
const { link } = useApmRouter();
const errorDetailsLink = link('/services/{serviceName}/errors/{groupId}', {
path: {
serviceName,
groupId: errorGroupId,
},
query,
});
return <EuiLink data-test-subj="apmErrorDetailsLink" href={errorDetailsLink} {...rest} />;
}
export { ErrorDetailLink };

View file

@ -6,11 +6,14 @@
*/
import React from 'react';
import type { APMLinkExtendProps } from './apm_link';
import { LegacyAPMLink } from './apm_link';
import { EuiLink } from '@elastic/eui';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import type { APMLinkExtendProps } from './apm_link_hooks';
function HomeLink(props: APMLinkExtendProps) {
return <LegacyAPMLink path="/" {...props} />;
const { link } = useApmRouter();
const homeLink = link('/');
return <EuiLink data-test-subj="apmHomeLink" href={homeLink} {...props} />;
}
export { HomeLink };

View file

@ -6,9 +6,13 @@
*/
import React from 'react';
import type { TypeOf } from '@kbn/typed-react-router-config';
import { EuiLink } from '@elastic/eui';
import type { ApmRoutes } from '../../../routing/apm_route_config';
import type { APMQueryParams } from '../url_helpers';
import type { APMLinkExtendProps } from './apm_link';
import { LegacyAPMLink, useAPMHref } from './apm_link';
import { useAPMHref } from './apm_link_hooks';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import type { APMLinkExtendProps } from './apm_link_hooks';
const persistedFilters: Array<keyof APMQueryParams> = [
'host',
@ -19,15 +23,24 @@ const persistedFilters: Array<keyof APMQueryParams> = [
export function useMetricOverviewHref(serviceName: string) {
return useAPMHref({
path: `/services/${serviceName}/metrics`,
path: `/services/{serviceName}/metrics`,
pathParams: { serviceName },
persistedFilters,
});
}
interface Props extends APMLinkExtendProps {
serviceName: string;
query: TypeOf<ApmRoutes, '/services/{serviceName}/metrics'>['query'];
}
export function MetricOverviewLink({ serviceName, ...rest }: Props) {
return <LegacyAPMLink path={`/services/${serviceName}/metrics`} {...rest} />;
export function MetricOverviewLink({ serviceName, query, ...rest }: Props) {
const { link } = useApmRouter();
const metricsOverviewLink = link('/services/{serviceName}/metrics', {
path: {
serviceName,
},
query,
});
return <EuiLink data-test-subj="apmMetricsOverviewLink" href={metricsOverviewLink} {...rest} />;
}

View file

@ -6,7 +6,7 @@
*/
import type { APMQueryParams } from '../url_helpers';
import { useAPMHref } from './apm_link';
import { useAPMHref } from './apm_link_hooks';
const persistedFilters: Array<keyof APMQueryParams> = ['host', 'agentName'];

View file

@ -5,21 +5,29 @@
* 2.0.
*/
import { EuiLink } from '@elastic/eui';
import React from 'react';
import type { APMLinkExtendProps } from './apm_link';
import { useAPMHref } from './apm_link';
export function useServiceMapHref(serviceName?: string) {
const path = serviceName ? `/services/${serviceName}/service-map` : '/service-map';
return useAPMHref({ path });
}
import type { TypeOf } from '@kbn/typed-react-router-config';
import { EuiLink } from '@elastic/eui';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import type { ApmRoutes } from '../../../routing/apm_route_config';
import type { APMLinkExtendProps } from './apm_link_hooks';
interface ServiceMapLinkProps extends APMLinkExtendProps {
serviceName?: string;
query:
| TypeOf<ApmRoutes, '/services/{serviceName}/service-map'>['query']
| TypeOf<ApmRoutes, '/service-map'>['query'];
}
export function ServiceMapLink({ serviceName, ...rest }: ServiceMapLinkProps) {
const href = useServiceMapHref(serviceName);
export function ServiceMapLink({ serviceName, query, ...rest }: ServiceMapLinkProps) {
const { link } = useApmRouter();
const href = serviceName
? link('/services/{serviceName}/service-map', {
path: {
serviceName,
},
query,
})
: link('/service-map', { query });
return <EuiLink data-test-subj="apmServiceMapLinkLink" href={href} {...rest} />;
}

View file

@ -8,8 +8,8 @@
import { EuiLink } from '@elastic/eui';
import React from 'react';
import type { APMQueryParams } from '../url_helpers';
import type { APMLinkExtendProps } from './apm_link';
import { useAPMHref } from './apm_link';
import type { APMLinkExtendProps } from './apm_link_hooks';
import { useAPMHref } from './apm_link_hooks';
interface Props extends APMLinkExtendProps {
serviceName: string;
@ -31,7 +31,8 @@ export function useServiceNodeMetricOverviewHref({
serviceNodeName: string;
}) {
return useAPMHref({
path: `/services/${serviceName}/metrics/${encodeURIComponent(serviceNodeName)}`,
path: '/services/{serviceName}/metrics/{serviceNodeName}',
pathParams: { serviceName, serviceNodeName },
persistedFilters,
});
}

View file

@ -6,7 +6,7 @@
*/
import type { APMQueryParams } from '../url_helpers';
import { useAPMHref } from './apm_link';
import { useAPMHref } from './apm_link_hooks';
const persistedFilters: Array<keyof APMQueryParams> = [
'host',
@ -17,7 +17,8 @@ const persistedFilters: Array<keyof APMQueryParams> = [
export function useServiceNodeOverviewHref(serviceName: string) {
return useAPMHref({
path: `/services/${serviceName}/nodes`,
path: '/services/{serviceName}/nodes',
pathParams: { serviceName },
persistedFilters,
});
}

View file

@ -8,8 +8,8 @@
import { EuiLink } from '@elastic/eui';
import React from 'react';
import type { APMQueryParams } from '../url_helpers';
import type { APMLinkExtendProps } from './apm_link';
import { useAPMHref } from './apm_link';
import type { APMLinkExtendProps } from './apm_link_hooks';
import { useAPMHref } from './apm_link_hooks';
import { removeUndefinedProps } from '../../../../context/url_params_context/helpers';
const persistedFilters: Array<keyof APMQueryParams> = [
@ -34,7 +34,8 @@ export function useServiceOrTransactionsOverviewHref({
}: Props) {
const query = { environment, transactionType };
return useAPMHref({
path: `/services/${serviceName}`,
path: '/services/{serviceName}',
pathParams: { serviceName },
persistedFilters,
query: removeUndefinedProps(query),
});

View file

@ -6,7 +6,7 @@
*/
import type { APMQueryParams } from '../url_helpers';
import { useAPMHref } from './apm_link';
import { useAPMHref } from './apm_link_hooks';
const persistedFilters: Array<keyof APMQueryParams> = [
'transactionResult',

View file

@ -10,51 +10,21 @@ import { getRenderedHref } from '../../../../../utils/test_helpers';
import { TransactionDetailLink } from '.';
describe('TransactionDetailLink', () => {
describe('With comparison in the url', () => {
it('returns comparison defined in the url', async () => {
const href = await getRenderedHref(
() => (
<TransactionDetailLink
serviceName="foo"
transactionName="bar"
transactionType="request"
comparisonEnabled
offset="1w"
traceId="baz"
transactionId="123"
>
Transaction
</TransactionDetailLink>
),
{} as Location
);
it('returns the correct url', async () => {
const href = await getRenderedHref(
() => (
<TransactionDetailLink
transactionName="bar"
href="/basepath/app/apm/services/foo/transactions/view?traceId=baz&transactionId=123&transactionName=bar&transactionType=request&comparisonEnabled=true&offset=1w"
>
Transaction
</TransactionDetailLink>
),
{} as Location
);
expect(href).toMatchInlineSnapshot(
'"/basepath/app/apm/services/foo/transactions/view?traceId=baz&transactionId=123&transactionName=bar&transactionType=request&comparisonEnabled=true&offset=1w"'
);
});
});
describe('use default comparison', () => {
it('returns default comparison', async () => {
const href = await getRenderedHref(
() => (
<TransactionDetailLink
serviceName="foo"
transactionName="bar"
transactionType="request"
traceId="baz"
transactionId="123"
>
Transaction
</TransactionDetailLink>
),
{} as Location
);
expect(href).toMatchInlineSnapshot(
'"/basepath/app/apm/services/foo/transactions/view?traceId=baz&transactionId=123&transactionName=bar&transactionType=request&comparisonEnabled=true&offset=1d"'
);
});
expect(href).toMatchInlineSnapshot(
'"/basepath/app/apm/services/foo/transactions/view?traceId=baz&transactionId=123&transactionName=bar&transactionType=request&comparisonEnabled=true&offset=1w"'
);
});
});

View file

@ -7,74 +7,21 @@
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { identity, pickBy } from 'lodash';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { pickKeys } from '../../../../../../common/utils/pick_keys';
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
import { useLegacyUrlParams } from '../../../../../context/url_params_context/use_url_params';
import { unit } from '../../../../../utils/style';
import { PopoverTooltip } from '../../../popover_tooltip';
import { getComparisonEnabled } from '../../../time_comparison/get_comparison_enabled';
import { TruncateWithTooltip } from '../../../truncate_with_tooltip';
import type { APMQueryParams } from '../../url_helpers';
import type { APMLinkExtendProps } from '../apm_link';
import { getLegacyApmHref } from '../apm_link';
import type { APMLinkExtendProps } from '../apm_link_hooks';
import { MaxGroupsMessage } from '../max_groups_message';
export const txGroupsDroppedBucketName = '_other';
interface Props extends APMLinkExtendProps {
serviceName: string;
traceId?: string;
transactionId?: string;
transactionName: string;
transactionType?: string;
latencyAggregationType?: string;
environment?: string;
comparisonEnabled?: boolean;
offset?: string;
overflowCount?: number;
href: string;
}
const persistedFilters: Array<keyof APMQueryParams> = ['transactionResult', 'serviceVersion'];
export function TransactionDetailLink({
serviceName,
traceId,
transactionId,
transactionName,
transactionType,
latencyAggregationType,
environment,
comparisonEnabled,
offset = '1d',
overflowCount = 0,
...rest
}: Props) {
const { urlParams } = useLegacyUrlParams();
const { core } = useApmPluginContext();
const defaultComparisonEnabled = getComparisonEnabled({
core,
urlComparisonEnabled: comparisonEnabled,
});
const location = useLocation();
const href = getLegacyApmHref({
basePath: core.http.basePath,
path: `/services/${serviceName}/transactions/view`,
query: {
traceId,
transactionId,
transactionName,
transactionType,
comparisonEnabled: defaultComparisonEnabled,
offset,
...pickKeys(urlParams as APMQueryParams, ...persistedFilters),
...pickBy({ latencyAggregationType, environment }, identity),
},
search: location.search,
});
export function TransactionDetailLink({ transactionName, href, ...rest }: Props) {
if (transactionName !== txGroupsDroppedBucketName) {
return (
<TruncateWithTooltip

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { render, renderHook } from '@testing-library/react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider';
import { TransactionOverviewLink, useTransactionsOverviewHref } from './transaction_overview_link';
import { TransactionOverviewLink } from './transaction_overview_link';
import type { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
const history = createMemoryHistory();
@ -23,53 +24,82 @@ function Wrapper({ children }: React.PropsWithChildren) {
}
describe('Transactions overview link', () => {
describe('useTransactionsOverviewHref', () => {
it('returns transaction link', () => {
const { result } = renderHook(() => useTransactionsOverviewHref({ serviceName: 'foo' }), {
wrapper: Wrapper,
});
expect(result.current).toEqual('/basepath/app/apm/services/foo/transactions');
});
it('returns transaction link with persisted query items', () => {
const { result } = renderHook(
() =>
useTransactionsOverviewHref({
serviceName: 'foo',
latencyAggregationType: 'avg',
}),
{ wrapper: Wrapper }
);
expect(result.current).toEqual(
'/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg'
);
});
});
describe('TransactionOverviewLink', () => {
function getHref(container: HTMLElement) {
return ((container as HTMLDivElement).children[0] as HTMLAnchorElement).href;
}
it('returns transaction link', () => {
it('returns transaction link with persisted query and prop items', () => {
const avg = 'avg' as LatencyAggregationType;
const { container } = render(
<Wrapper>
<TransactionOverviewLink serviceName="foo">Service name</TransactionOverviewLink>
</Wrapper>
);
expect(getHref(container)).toEqual(
'http://localhost/basepath/app/apm/services/foo/transactions'
);
});
it('returns transaction link with persisted query items', () => {
const { container } = render(
<Wrapper>
<TransactionOverviewLink serviceName="foo" latencyAggregationType="avg">
<TransactionOverviewLink
serviceName="foo"
latencyAggregationType={avg}
transactionType="request"
query={{
environment: 'production',
rangeFrom: 'now-15m',
rangeTo: 'now',
kuery: '',
serviceGroup: '',
comparisonEnabled: false,
}}
>
Service name
</TransactionOverviewLink>
</Wrapper>
);
expect(getHref(container)).toEqual(
'http://localhost/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg'
'http://localhost/basepath/app/apm/services/foo/transactions?comparisonEnabled=false&environment=production&kuery=&latencyAggregationType=avg&rangeFrom=now-15m&rangeTo=now&serviceGroup=&transactionType=request'
);
});
it('returns transaction link with persisted without transaction type', () => {
const avg = 'avg' as LatencyAggregationType;
const { container } = render(
<Wrapper>
<TransactionOverviewLink
serviceName="foo"
latencyAggregationType={avg}
query={{
environment: 'production',
rangeFrom: 'now-15m',
rangeTo: 'now',
kuery: '',
serviceGroup: '',
comparisonEnabled: false,
}}
>
Service name
</TransactionOverviewLink>
</Wrapper>
);
expect(getHref(container)).toEqual(
'http://localhost/basepath/app/apm/services/foo/transactions?comparisonEnabled=false&environment=production&kuery=&latencyAggregationType=avg&rangeFrom=now-15m&rangeTo=now&serviceGroup='
);
});
it('returns transaction link with persisted query with transaction type', () => {
const avg = 'avg' as LatencyAggregationType;
const { container } = render(
<Wrapper>
<TransactionOverviewLink
serviceName="foo"
latencyAggregationType={avg}
query={{
environment: 'production',
rangeFrom: 'now-15m',
rangeTo: 'now',
kuery: '',
serviceGroup: '',
comparisonEnabled: false,
transactionType: 'request',
}}
>
Service name
</TransactionOverviewLink>
</Wrapper>
);
expect(getHref(container)).toEqual(
'http://localhost/basepath/app/apm/services/foo/transactions?comparisonEnabled=false&environment=production&kuery=&latencyAggregationType=avg&rangeFrom=now-15m&rangeTo=now&serviceGroup=&transactionType=request'
);
});
});

View file

@ -7,47 +7,36 @@
import { EuiLink } from '@elastic/eui';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { removeUndefinedProps } from '../../../../context/url_params_context/helpers';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import type { APMLinkExtendProps } from './apm_link';
import { getLegacyApmHref } from './apm_link';
import type { TypeOf } from '@kbn/typed-react-router-config/src/types';
import type { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import type { APMLinkExtendProps } from './apm_link_hooks';
import type { ApmRoutes } from '../../../routing/apm_route_config';
interface Props extends APMLinkExtendProps {
serviceName: string;
latencyAggregationType?: string;
latencyAggregationType?: LatencyAggregationType;
transactionType?: string;
}
export function useTransactionsOverviewHref({
serviceName,
latencyAggregationType,
transactionType,
}: Props) {
const { core } = useApmPluginContext();
const location = useLocation();
const { search } = location;
const query = { latencyAggregationType, transactionType };
return getLegacyApmHref({
basePath: core.http.basePath,
path: `/services/${serviceName}/transactions`,
query: removeUndefinedProps(query),
search,
});
query: TypeOf<ApmRoutes, '/services/{serviceName}/transactions'>['query'];
}
export function TransactionOverviewLink({
serviceName,
latencyAggregationType,
transactionType,
query,
...rest
}: Props) {
const href = useTransactionsOverviewHref({
serviceName,
latencyAggregationType,
transactionType,
const { link } = useApmRouter();
const href = link('/services/{serviceName}/transactions', {
path: { serviceName },
query: {
...query,
latencyAggregationType,
transactionType: transactionType ?? query.transactionType,
},
});
return <EuiLink data-test-subj="apmTransactionOverviewLinkLink" href={href} {...rest} />;
}

View file

@ -66,7 +66,7 @@ export interface APMQueryParams {
waterfallItemId?: string;
spanId?: string;
page?: string | number;
pageSize?: string;
pageSize?: string | number;
sortDirection?: string;
sortField?: string;
kuery?: string;

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state';
import { useMlManageJobsHref } from '../../../hooks/use_ml_manage_jobs_href';
import { useAPMHref } from '../links/apm/apm_link';
import { useAPMHref } from '../links/apm/apm_link_hooks';
export function shouldDisplayMlCallout(anomalyDetectionSetupState: AnomalyDetectionSetupState) {
return (

View file

@ -99,8 +99,10 @@ describe('TimeComparison component', () => {
);
jest.spyOn(useEnvironmentContextModule, 'useEnvironmentsContext').mockReturnValue({
// @ts-ignore mocking only partial data
preferredEnvironment: 'prod',
environment: 'prod',
environments: [],
status: FETCH_STATUS.SUCCESS,
});
};
beforeAll(() => {
@ -152,8 +154,10 @@ describe('TimeComparison component', () => {
it('shows enabled option for expected bounds when there are ML jobs available matching the preferred environment', () => {
jest.spyOn(useEnvironmentContextModule, 'useEnvironmentsContext').mockReturnValueOnce({
// @ts-ignore mocking only partial data
preferredEnvironment: 'prod',
environment: 'prod',
environments: [],
status: FETCH_STATUS.SUCCESS,
});
const Wrapper = getWrapper({

View file

@ -6,11 +6,18 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiButtonEmpty, EuiIcon } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
EuiButtonEmpty,
EuiIcon,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { NO_PERMISSION_LABEL } from '../../../../../common/custom_link';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { LegacyAPMLink } from '../../links/apm/apm_link';
export function CustomLinkToolbar({
onClickCreate,
@ -21,6 +28,7 @@ export function CustomLinkToolbar({
}) {
const { core } = useApmPluginContext();
const canSave = !!core.application.capabilities.apm.save;
const { link } = useApmRouter();
return (
<EuiFlexGroup>
@ -33,15 +41,28 @@ export function CustomLinkToolbar({
defaultMessage: 'Manage custom links',
})}
>
<LegacyAPMLink path={`/settings/custom-links`}>
<EuiIcon type="gear" color="text" aria-label="Custom links settings page" />
</LegacyAPMLink>
<EuiLink
data-test-subj="apmCustomLinksSettingsPage"
href={link('/settings/custom-links')}
>
<EuiIcon
type="gear"
color="text"
aria-label={i18n.translate(
'xpack.apm.customLinkToolbar.euiIcon.customLinksSettingsPageLabel',
{ defaultMessage: 'Custom links settings page' }
)}
/>
</EuiLink>
</EuiToolTip>
</EuiFlexItem>
{showCreateButton && (
<EuiToolTip content={!canSave && NO_PERMISSION_LABEL}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
aria-label={i18n.translate('xpack.apm.customLinkToolbar.createButton.ariaLabel', {
defaultMessage: 'Create',
})}
data-test-subj="apmCustomLinkToolbarCreateButton"
isDisabled={!canSave}
iconType="plusInCircle"

View file

@ -63,7 +63,7 @@ export function getColumns({
latencyAggregationType?: LatencyAggregationType;
detailedStatisticsLoading: boolean;
detailedStatistics?: TransactionGroupDetailedStatistics;
comparisonEnabled?: boolean;
comparisonEnabled: boolean;
shouldShowSparkPlots?: boolean;
showAlertsColumn: boolean;
offset?: string;
@ -131,13 +131,17 @@ export function getColumns({
render: (_, { name, transactionType: type }) => {
return (
<TransactionDetailLink
serviceName={serviceName}
transactionName={name}
transactionType={type}
latencyAggregationType={latencyAggregationType}
comparisonEnabled={comparisonEnabled}
offset={offset}
overflowCount={transactionOverflowCount}
href={link('/services/{serviceName}/transactions/view', {
path: { serviceName },
query: {
...query,
transactionName: name,
transactionType: type,
comparisonEnabled,
offset,
},
})}
>
{name}
</TransactionDetailLink>

View file

@ -29,6 +29,7 @@ import { OverviewTableContainer } from '../overview_table_container';
import { isTimeComparison } from '../time_comparison/get_comparison_options';
import { getColumns } from './get_columns';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { getComparisonEnabled } from '../time_comparison/get_comparison_enabled';
type ApiResponse =
APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics'>;
@ -71,6 +72,7 @@ export function TransactionsTable({
showSparkPlots,
}: Props) {
const { link } = useApmRouter();
const { core, observabilityAIAssistant } = useApmPluginContext();
const [renderedItemIndices, setRenderedItemIndices] = useState<VisibleItemsStartEnd>([0, 0]);
const {
@ -80,11 +82,17 @@ export function TransactionsTable({
'/services/{serviceName}/transactions',
'/services/{serviceName}/overview',
'/mobile-services/{serviceName}/transactions',
'/mobile-services/{serviceName}/overview'
'/mobile-services/{serviceName}/overview',
'/services/{serviceName}/transactions/view'
);
const latencyAggregationType = getLatencyAggregationType(latencyAggregationTypeFromQuery);
const defaultComparisonEnabled = getComparisonEnabled({
core,
urlComparisonEnabled: comparisonEnabled,
});
const { isLarge } = useBreakpoints();
const shouldShowSparkPlots = showSparkPlots ?? !isLarge;
const { transactionType, serviceName } = useApmServiceContext();
@ -111,7 +119,7 @@ export function TransactionsTable({
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
detailedStatisticsLoading: isPending(detailedStatisticsStatus),
detailedStatistics,
comparisonEnabled,
comparisonEnabled: defaultComparisonEnabled,
shouldShowSparkPlots,
offset,
transactionOverflowCount: mainStatistics.transactionOverflowCount,
@ -120,7 +128,7 @@ export function TransactionsTable({
query,
});
}, [
comparisonEnabled,
defaultComparisonEnabled,
detailedStatistics,
detailedStatisticsStatus,
latencyAggregationType,
@ -133,7 +141,6 @@ export function TransactionsTable({
shouldShowSparkPlots,
]);
const { core, observabilityAIAssistant } = useApmPluginContext();
const setScreenContext = observabilityAIAssistant?.service.setScreenContext;
const isTableSearchBarEnabled = core.uiSettings.get<boolean>(apmEnableTableSearchBar, true);
@ -187,6 +194,7 @@ export function TransactionsTable({
serviceName={serviceName}
latencyAggregationType={latencyAggregationType}
transactionType={transactionType}
query={query}
>
{i18n.translate('xpack.apm.transactionsTable.linkText', {
defaultMessage: 'View transactions',

View file

@ -30,7 +30,7 @@ export class ServiceOverviewLocatorDefinition implements LocatorDefinition<Servi
const params = { rangeFrom, rangeTo, environment };
return {
app: 'apm',
path: `/services/${serviceName}/overview?${qs.stringify(params)}`,
path: `/services/${encodeURIComponent(serviceName)}/overview?${qs.stringify(params)}`,
state: {},
};
};

View file

@ -63,4 +63,16 @@ describe('getLegacyApmHref', () => {
);
});
});
describe('with service.name that needs encoding', () => {
const serviceNameWithSlashes = 'My/Service/Name';
beforeEach(() => {
summary.state.service = { name: serviceNameWithSlashes };
});
it('links to the service with encoding', () => {
const result = getLegacyApmHref(summary, 'foo', 'now-15m', 'now');
expect(result).toMatchInlineSnapshot(
`"foo/app/apm/services/My%2FService%2FName/overview/?rangeFrom=now-15m&rangeTo=now"`
);
});
});
});

View file

@ -19,7 +19,9 @@ export const getLegacyApmHref = (
if (serviceName) {
return addBasePath(
basePath,
`/app/apm/services/${serviceName}/overview/?rangeFrom=${dateRangeStart}&rangeTo=${dateRangeEnd}`
`/app/apm/services/${encodeURIComponent(
serviceName
)}/overview/?rangeFrom=${dateRangeStart}&rangeTo=${dateRangeEnd}`
);
}