mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# 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:
parent
fde4a3d9ea
commit
906ff07331
52 changed files with 661 additions and 382 deletions
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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('/*');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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} />;
|
||||
}
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue