diff --git a/src/platform/packages/shared/kbn-apm-synthtrace/src/scenarios/trace_with_service_names_with_slashes.ts b/src/platform/packages/shared/kbn-apm-synthtrace/src/scenarios/trace_with_service_names_with_slashes.ts new file mode 100644 index 0000000000000..b62bd2fd77336 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-synthtrace/src/scenarios/trace_with_service_names_with_slashes.ts @@ -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 = 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; diff --git a/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.test.ts b/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.test.ts index 5c128cf57fefc..bd59eb8f64ea7 100644 --- a/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.test.ts +++ b/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.test.ts @@ -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); diff --git a/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.ts b/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.ts index 291ba67cf70fd..5c0c08341ee76 100644 --- a/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.ts +++ b/src/platform/packages/shared/kbn-server-route-repository-utils/src/format_request.ts @@ -21,8 +21,8 @@ export function formatRequest(endpoint: string, pathParams: Record 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) { diff --git a/src/platform/packages/shared/kbn-typed-react-router-config/index.ts b/src/platform/packages/shared/kbn-typed-react-router-config/index.ts index 6aff73c55e6c3..3efecb2d77d43 100644 --- a/src/platform/packages/shared/kbn-typed-react-router-config/index.ts +++ b/src/platform/packages/shared/kbn-typed-react-router-config/index.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-typed-react-router-config/src/create_router.ts b/src/platform/packages/shared/kbn-typed-react-router-config/src/create_router.ts index 4321f4c0744a7..4451603f33fa6 100644 --- a/src/platform/packages/shared/kbn-typed-react-router-config/src/create_router.ts +++ b/src/platform/packages/shared/kbn-typed-react-router-config/src/create_router.ts @@ -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(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); diff --git a/src/platform/packages/shared/kbn-typed-react-router-config/src/encode_path.test.ts b/src/platform/packages/shared/kbn-typed-react-router-config/src/encode_path.test.ts new file mode 100644 index 0000000000000..7bff3d81d7bc9 --- /dev/null +++ b/src/platform/packages/shared/kbn-typed-react-router-config/src/encode_path.test.ts @@ -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'); + }); +}); diff --git a/src/platform/packages/shared/kbn-typed-react-router-config/src/encode_path.ts b/src/platform/packages/shared/kbn-typed-react-router-config/src/encode_path.ts new file mode 100644 index 0000000000000..c86da50b53b4d --- /dev/null +++ b/src/platform/packages/shared/kbn-typed-react-router-config/src/encode_path.ts @@ -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) => + 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; diff --git a/x-pack/solutions/observability/plugins/apm/common/environment_rt.ts b/x-pack/solutions/observability/plugins/apm/common/environment_rt.ts index 530b6e7930134..87d65a0847c30 100644 --- a/x-pack/solutions/observability/plugins/apm/common/environment_rt.ts +++ b/x-pack/solutions/observability/plugins/apm/common/environment_rt.ts @@ -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, ]); diff --git a/x-pack/solutions/observability/plugins/apm/dev_docs/routing_and_linking.md b/x-pack/solutions/observability/plugins/apm/dev_docs/routing_and_linking.md index 19087d12dbc9a..946e1a05569fa 100644 --- a/x-pack/solutions/observability/plugins/apm/dev_docs/routing_and_linking.md +++ b/x-pack/solutions/observability/plugins/apm/dev_docs/routing_and_linking.md @@ -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 diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx index 149e96c419fb7..659f677f8d6f6 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx @@ -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({ })} > {transaction.transaction.name} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx index 32fe2c2e103dc..660249741b23b 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/top_erroneous_transactions/index.tsx @@ -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={ {transactionName} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx index 39d62e3c3a3a5..c0a170d819b63 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx @@ -129,6 +129,7 @@ export function ErrorGroupList({ {groupId.slice(0, 5) || NOT_AVAILABLE_LABEL} @@ -172,7 +173,7 @@ export function ErrorGroupList({ return ( - + {item.name || NOT_AVAILABLE_LABEL} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/controls.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/controls.tsx index 39490ab8e37c5..55e9a112fdff9 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/controls.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/controls.tsx @@ -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'; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index 85da44144453a..715fa91fcbbdc 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -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; shouldShowSparkPlots?: boolean; + query: Omit['query'], 'kuery'>; }): Array> { return [ { @@ -75,12 +79,12 @@ export function getColumns({ const link = ( ({ + query={{ ...query, kuery: isMissingServiceNodeName ? `NOT (service.node.name:*)` : `service.node.name:"${item.serviceNodeName}"`, - })} + }} > {text} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index 073c39dfa4186..ee81e265c5b2b 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -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 = { diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx index b23afedb6cd84..049261d2579c5 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx @@ -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) { {/* Cancel button */} - - + + {i18n.translate('xpack.apm.agentConfig.servicePage.cancelButton', { defaultMessage: 'Cancel', })} - + {/* Next button */} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_explorer/agent_instances/agent_instances_details/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_explorer/agent_instances/agent_instances_details/index.tsx index 3f2e67ea07071..8e066e55c7a4d 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_explorer/agent_instances/agent_instances_details/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/settings/agent_explorer/agent_instances/agent_instances_details/index.tsx @@ -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> { +interface GetInstanceColumnsProps { + serviceName: string; + agentName: AgentName; + query: Omit['query'], 'kuery'>; + agentDocsPageUrl?: string; +} + +export function getInstanceColumns({ + serviceName, + agentName, + query, + agentDocsPageUrl, +}: GetInstanceColumnsProps): Array> { return [ { field: AgentExplorerInstanceFieldName.InstanceName, @@ -64,7 +78,7 @@ export function getInstanceColumns( } )} > - +

({ + query={{ ...query, kuery: `service.node.name:"${displayedName}"`, - })} + }} > {displayedName} @@ -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 ( <> + {i18n.translate( 'xpack.apm.settings.schema.success.returnText.serviceInventoryLink', { defaultMessage: 'Service inventory' } )} - + ), }} /> diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/top_traces_overview/trace_list.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/top_traces_overview/trace_list.tsx index e12f701de9606..008219148ec84 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/top_traces_overview/trace_list.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/top_traces_overview/trace_list.tsx @@ -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['data']['items'][number]; export function getTraceListColumns({ query, + link, }: { query: TypeOf['query']; + link: ( + path: '/services/{serviceName}/transactions/view', + params: { + path: { serviceName: string }; + query: TypeOf['query']; + } + ) => string; }): Array> { return [ { @@ -54,9 +63,17 @@ export function getTraceListColumns({ render: (_: string, { serviceName, transactionName, transactionType }: TraceGroup) => ( {transactionName} @@ -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) { diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx index a7257663dd413..ca2bd52d261e8 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx @@ -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 ( diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx index 2c76d5dc73664..de0c21aa31b6f 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx @@ -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: ( {transaction.transaction.name} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx index 441b0235914ae..6c79c6ddf3d9a 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx @@ -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: ( {transaction.transaction.name} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/anomaly_detection_setup_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/anomaly_detection_setup_link.tsx index b843a709de34b..a8c80b546db3d 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/anomaly_detection_setup_link.tsx @@ -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('/*'); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/index.tsx index 06d04c94cfddd..8b903c49da16a 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/routing/app_root/apm_header_action_menu/index.tsx @@ -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'; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx index 58e0ab63fedb3..871a431940711 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx @@ -16,7 +16,11 @@ import { ErrorMarker } from './error_marker'; function Wrapper({ children }: { children?: ReactNode }) { return ( - + {children} ); @@ -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); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.tsx index acef8e56551f8..a2f40b33ec8f2 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/charts/timeline/marker/error_marker.tsx @@ -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} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/errors_table/get_columns.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/errors_table/get_columns.tsx index a671bb0220259..b64fcba7729e7 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/errors_table/get_columns.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/errors_table/get_columns.tsx @@ -88,7 +88,7 @@ export function getColumns({ + {name} } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/agent_configuration_links.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/agent_configuration_links.tsx index 2e1030653d5c6..7cd0779e19cc2 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/agent_configuration_links.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/agent_configuration_links.tsx @@ -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'], diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link.test.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link.test.tsx deleted file mode 100644 index 4f889f2eda5a6..0000000000000 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link.test.tsx +++ /dev/null @@ -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( - () => , - { - 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( - () => , - { - 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( - () => , - { - 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"` - ); - }); -}); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link_hooks.ts similarity index 79% rename from x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link.tsx rename to x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link_hooks.ts index 6d3d57bc6b660..940ae9fd09cd9 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/apm_link_hooks.ts @@ -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; query?: APMQueryParams; + pathParams?: Record; }) { 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 ; -} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/error_detail_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/error_detail_link.tsx index 74ef4682f4cc1..8cb45e026993c 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/error_detail_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/error_detail_link.tsx @@ -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['query']; } -function ErrorDetailLink({ serviceName, errorGroupId, ...rest }: Props) { - return ; +function ErrorDetailLink({ serviceName, errorGroupId, query, ...rest }: Props) { + const { link } = useApmRouter(); + const errorDetailsLink = link('/services/{serviceName}/errors/{groupId}', { + path: { + serviceName, + groupId: errorGroupId, + }, + query, + }); + return ; } export { ErrorDetailLink }; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/home_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/home_link.tsx index 9d7b27b24daed..33b39df6cc478 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/home_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/home_link.tsx @@ -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 ; + const { link } = useApmRouter(); + const homeLink = link('/'); + return ; } export { HomeLink }; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/metric_overview_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/metric_overview_link.tsx index e77b77537ce52..001bc771b3b28 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/metric_overview_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/metric_overview_link.tsx @@ -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 = [ 'host', @@ -19,15 +23,24 @@ const persistedFilters: Array = [ 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['query']; } -export function MetricOverviewLink({ serviceName, ...rest }: Props) { - return ; +export function MetricOverviewLink({ serviceName, query, ...rest }: Props) { + const { link } = useApmRouter(); + const metricsOverviewLink = link('/services/{serviceName}/metrics', { + path: { + serviceName, + }, + query, + }); + return ; } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_inventory_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_inventory_link.tsx index 4d95f3c70a483..6327ab1c91b84 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_inventory_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_inventory_link.tsx @@ -6,7 +6,7 @@ */ import type { APMQueryParams } from '../url_helpers'; -import { useAPMHref } from './apm_link'; +import { useAPMHref } from './apm_link_hooks'; const persistedFilters: Array = ['host', 'agentName']; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_map_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_map_link.tsx index f9958e588ba73..78981e0422ce4 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_map_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_map_link.tsx @@ -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['query'] + | TypeOf['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 ; } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_metric_overview_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_metric_overview_link.tsx index 7b99e981e9bb1..397ff4456f526 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_metric_overview_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_metric_overview_link.tsx @@ -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, }); } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_overview_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_overview_link.tsx index 7ea28dfbebeac..8452738820a0d 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_overview_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_node_overview_link.tsx @@ -6,7 +6,7 @@ */ import type { APMQueryParams } from '../url_helpers'; -import { useAPMHref } from './apm_link'; +import { useAPMHref } from './apm_link_hooks'; const persistedFilters: Array = [ 'host', @@ -17,7 +17,8 @@ const persistedFilters: Array = [ export function useServiceNodeOverviewHref(serviceName: string) { return useAPMHref({ - path: `/services/${serviceName}/nodes`, + path: '/services/{serviceName}/nodes', + pathParams: { serviceName }, persistedFilters, }); } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.tsx index edf62a082bc56..0fbcd4fb4d6e1 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/service_transactions_overview_link.tsx @@ -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 = [ @@ -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), }); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/trace_overview_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/trace_overview_link.tsx index 2462c5905c5e4..9eb70c34d393d 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/trace_overview_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/trace_overview_link.tsx @@ -6,7 +6,7 @@ */ import type { APMQueryParams } from '../url_helpers'; -import { useAPMHref } from './apm_link'; +import { useAPMHref } from './apm_link_hooks'; const persistedFilters: Array = [ 'transactionResult', diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.test.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.test.tsx index 5c405c711c8b3..88d023700fdfc 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.test.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.test.tsx @@ -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( - () => ( - - Transaction - - ), - {} as Location - ); + it('returns the correct url', async () => { + const href = await getRenderedHref( + () => ( + + Transaction + + ), + {} 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( - () => ( - - Transaction - - ), - {} 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"' + ); }); }); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.tsx index 78904a0d8c389..4f0b1097f89a7 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_detail_link/index.tsx @@ -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 = ['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 ( { - 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( - Service name + + Service name + ); expect(getHref(container)).toEqual( - 'http://localhost/basepath/app/apm/services/foo/transactions' + '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 query items', () => { + it('returns transaction link with persisted without transaction type', () => { + const avg = 'avg' as LatencyAggregationType; + const { container } = render( + + + Service name + + + ); + 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( - + Service name ); 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' ); }); }); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_overview_link.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_overview_link.tsx index 93e0382794b76..d40e0a942b16d 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_overview_link.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/apm/transaction_overview_link.tsx @@ -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['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 ; } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/url_helpers.ts b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/url_helpers.ts index 06e3b31792858..1067123481494 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/links/url_helpers.ts +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/links/url_helpers.ts @@ -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; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/ml_callout/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/ml_callout/index.tsx index 0a6058a2c28c6..3a8671f384811 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/ml_callout/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/ml_callout/index.tsx @@ -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 ( diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/time_comparison/index.test.tsx index 21f3662ec8f55..d67555117d53a 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/time_comparison/index.test.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/time_comparison/index.test.tsx @@ -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({ diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.tsx index 7c6734cf3d598..1139ac926fa67 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.tsx @@ -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 ( @@ -33,15 +41,28 @@ export function CustomLinkToolbar({ defaultMessage: 'Manage custom links', })} > - - - + + + {showCreateButton && ( { return ( {name} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/transactions_table/index.tsx index 4bfffc731cbe2..8c30eec5a9376 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -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([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(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', diff --git a/x-pack/solutions/observability/plugins/observability_shared/common/locators/apm/service_overview_locator.ts b/x-pack/solutions/observability/plugins/observability_shared/common/locators/apm/service_overview_locator.ts index e216640f31b4f..e7f478a788e55 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/common/locators/apm/service_overview_locator.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/common/locators/apm/service_overview_locator.ts @@ -30,7 +30,7 @@ export class ServiceOverviewLocatorDefinition implements LocatorDefinition { ); }); }); + 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"` + ); + }); + }); }); diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/lib/helper/observability_integration/get_apm_href.ts b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/lib/helper/observability_integration/get_apm_href.ts index a8089c92f95b8..fe08e37d105cb 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/lib/helper/observability_integration/get_apm_href.ts +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/lib/helper/observability_integration/get_apm_href.ts @@ -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}` ); }