mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
ESLint Telemetry Rule (#153108)
Resolves https://github.com/elastic/kibana/issues/144887 ## Summary This PR adds an ESLint Plugin which checks specific `Eui` elements for the existence of a `data-test-subj` prop. This rule will make having one for these elements required. This rule is currently only enabled for Observability apps (APM, Infra, Observability, Synthetics, Uptime). The plugin is also able to generate a suggestion based on the context in which the element is used. In the IDE this suggestion can be applied by using the autofix capability (see video below). When opening a PR, the CI will automatically apply the suggestion to qualifying Eui elements in the branch. https://user-images.githubusercontent.com/535564/225449622-bbfccb40-fdd2-4f69-9d5a-7d5a97bf62e6.mov ## Why do this? There is an increased push to move towards data driven feature development. In order to facilitate this, we need to have an increased focus on instrumenting user event generating elements in the Kibana codebase. This linting rule is an attempt to nudge Kibana engineers to not forget to add this property when writing frontend code. It also saves a bit of work for engineers by suggesting a value for the `data-test-subj` based on the location of the file in the codebase and any potential default values that might be present in the JSX node tree. Finally, because the suggestion is always of the same form, it can increase the consistency in the values given to these elements. ## Shape of the suggestion The suggestion for the value of data-test-subj is of the form: `[app][componentName][intent][euiElementName]`. For example, when working in a component in the location: `x-pack/plugins/observability/public/pages/overview/containers/overview_page/header_actions.tsx`, and having the code: ``` function HeaderActions() { return ( <EuiButton>{i18n.translate('id', { defaultMessage: 'Submit Form' })}</EuiButton> ) } ``` the suggestion becomes: `data-test-subj=o11yHeaderActionsSubmitFormButton`. For elements that don't take a `defaultMessage` prop / translation, the suggestion takes the form: `[app][componentName][euiElementName]` ## Which elements are checked by the ESLint rule? In its current iteration the rule checks these Eui elements: * `EuiButton` * `EuiButtonEmpty` * `EuiLink` * `EuiFieldText` * `EuiFieldSearch` * `EuiFieldNumber` * `EuiSelect` * `EuiRadioGroup` * 'EuiTextArea` ## What types of prop setting does this rule support? * `<EuiButton data-test-subj="foo">` (direct prop) * `<EuiButton {...foo}>` (via spreaded object; rule checks for `data-test-subj` key in object) ## What types of function declarations does this rule support? * `function Foo(){}` (Named function) * `const Foo = () => {}` (Arrow function assigned to variable) * `const Foo = memo(() => {})` (Arrow function assigned to variable wrapped in function) * `const Foo = hoc(uponHoc(uponHoc(() => {})))` (Arrow function assigned to variable wrapped in infinite levels of functions) ## Things to note * If an element already has a value for `data-test-subj` the rule will not kick in as any existing instrumentation might depend on the value. * the auto suggestion is just a suggestion: the engineer can always adjust the value for a `data-test-subj` before or after committing. Once a value is present (autofixed or manually set) the rule will not kick in. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dario Gieselaar <d.gieselaar@gmail.com> Co-authored-by: Katerina Patticha <kate@kpatticha.com> Co-authored-by: Tiago Costa <tiago.costa@elastic.co>
This commit is contained in:
parent
1892bf6535
commit
010ee2e112
346 changed files with 2029 additions and 391 deletions
12
.eslintrc.js
12
.eslintrc.js
|
@ -897,6 +897,18 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'x-pack/plugins/apm/**/*.{js,mjs,ts,tsx}',
|
||||
'x-pack/plugins/observability/**/*.{js,mjs,ts,tsx}',
|
||||
'x-pack/plugins/ux/**/*.{js,mjs,ts,tsx}',
|
||||
'x-pack/plugins/synthetics/**/*.{js,mjs,ts,tsx}',
|
||||
'x-pack/plugins/infra/**/*.{js,mjs,ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'@kbn/telemetry/event_generating_elements_should_be_instrumented': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
// require explicit return types in route handlers for performance reasons
|
||||
files: ['x-pack/plugins/apm/server/**/route.ts'],
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -334,6 +334,7 @@ packages/kbn-eslint-config @elastic/kibana-operations
|
|||
packages/kbn-eslint-plugin-disable @elastic/kibana-operations
|
||||
packages/kbn-eslint-plugin-eslint @elastic/kibana-operations
|
||||
packages/kbn-eslint-plugin-imports @elastic/kibana-operations
|
||||
packages/kbn-eslint-plugin-telemetry @elastic/actionable-observability
|
||||
x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security
|
||||
src/plugins/event_annotation @elastic/kibana-visualizations
|
||||
x-pack/test/plugin_api_integration/plugins/event_log @elastic/response-ops
|
||||
|
|
|
@ -1059,6 +1059,7 @@
|
|||
"@kbn/eslint-plugin-disable": "link:packages/kbn-eslint-plugin-disable",
|
||||
"@kbn/eslint-plugin-eslint": "link:packages/kbn-eslint-plugin-eslint",
|
||||
"@kbn/eslint-plugin-imports": "link:packages/kbn-eslint-plugin-imports",
|
||||
"@kbn/eslint-plugin-telemetry": "link:packages/kbn-eslint-plugin-telemetry",
|
||||
"@kbn/expect": "link:packages/kbn-expect",
|
||||
"@kbn/failed-test-reporter-cli": "link:packages/kbn-failed-test-reporter-cli",
|
||||
"@kbn/find-used-node-modules": "link:packages/kbn-find-used-node-modules",
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
const { USES_STYLED_COMPONENTS } = require('@kbn/babel-preset/styled_components_files');
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'./javascript.js',
|
||||
'./typescript.js',
|
||||
'./jest.js',
|
||||
'./react.js',
|
||||
],
|
||||
extends: ['./javascript.js', './typescript.js', './jest.js', './react.js'],
|
||||
|
||||
plugins: [
|
||||
'@kbn/eslint-plugin-disable',
|
||||
'@kbn/eslint-plugin-eslint',
|
||||
'@kbn/eslint-plugin-imports',
|
||||
'@kbn/eslint-plugin-telemetry',
|
||||
'prettier',
|
||||
],
|
||||
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018
|
||||
ecmaVersion: 2018,
|
||||
},
|
||||
|
||||
env: {
|
||||
|
@ -41,7 +37,7 @@ module.exports = {
|
|||
{
|
||||
from: 'mkdirp',
|
||||
to: false,
|
||||
disallowedMessage: `Don't use 'mkdirp', use the new { recursive: true } option of Fs.mkdir instead`
|
||||
disallowedMessage: `Don't use 'mkdirp', use the new { recursive: true } option of Fs.mkdir instead`,
|
||||
},
|
||||
{
|
||||
from: 'numeral',
|
||||
|
@ -50,7 +46,7 @@ module.exports = {
|
|||
{
|
||||
from: '@kbn/elastic-idx',
|
||||
to: false,
|
||||
disallowedMessage: `Don't use idx(), use optional chaining syntax instead https://ela.st/optchain`
|
||||
disallowedMessage: `Don't use idx(), use optional chaining syntax instead https://ela.st/optchain`,
|
||||
},
|
||||
{
|
||||
from: 'x-pack',
|
||||
|
@ -67,46 +63,45 @@ module.exports = {
|
|||
{
|
||||
from: 'monaco-editor',
|
||||
to: false,
|
||||
disallowedMessage: `Don't import monaco directly, use or add exports to @kbn/monaco`
|
||||
disallowedMessage: `Don't import monaco directly, use or add exports to @kbn/monaco`,
|
||||
},
|
||||
{
|
||||
from: 'tinymath',
|
||||
to: '@kbn/tinymath',
|
||||
disallowedMessage: `Don't use 'tinymath', use '@kbn/tinymath'`
|
||||
disallowedMessage: `Don't use 'tinymath', use '@kbn/tinymath'`,
|
||||
},
|
||||
{
|
||||
from: '@kbn/test/types/ftr',
|
||||
to: '@kbn/test',
|
||||
disallowedMessage: `import from the root of @kbn/test instead`
|
||||
disallowedMessage: `import from the root of @kbn/test instead`,
|
||||
},
|
||||
{
|
||||
from: 'react-intl',
|
||||
to: '@kbn/i18n-react',
|
||||
disallowedMessage: `import from @kbn/i18n-react instead`
|
||||
disallowedMessage: `import from @kbn/i18n-react instead`,
|
||||
},
|
||||
{
|
||||
from: 'styled-components',
|
||||
to: false,
|
||||
exclude: USES_STYLED_COMPONENTS,
|
||||
disallowedMessage: `Prefer using @emotion/react instead. To use styled-components, ensure you plugin is enabled in packages/kbn-babel-preset/styled_components_files.js.`
|
||||
disallowedMessage: `Prefer using @emotion/react instead. To use styled-components, ensure you plugin is enabled in packages/kbn-babel-preset/styled_components_files.js.`,
|
||||
},
|
||||
...[
|
||||
'@elastic/eui/dist/eui_theme_light.json',
|
||||
'@elastic/eui/dist/eui_theme_dark.json',
|
||||
].map(from => ({
|
||||
from,
|
||||
to: false,
|
||||
disallowedMessage: `Use "@kbn/ui-theme" to access theme vars.`
|
||||
})),
|
||||
...['@elastic/eui/dist/eui_theme_light.json', '@elastic/eui/dist/eui_theme_dark.json'].map(
|
||||
(from) => ({
|
||||
from,
|
||||
to: false,
|
||||
disallowedMessage: `Use "@kbn/ui-theme" to access theme vars.`,
|
||||
})
|
||||
),
|
||||
{
|
||||
from: '@kbn/test/jest',
|
||||
to: '@kbn/test-jest-helpers',
|
||||
disallowedMessage: `import from @kbn/test-jest-helpers instead`
|
||||
disallowedMessage: `import from @kbn/test-jest-helpers instead`,
|
||||
},
|
||||
{
|
||||
from: '@kbn/utility-types/jest',
|
||||
to: '@kbn/utility-types-jest',
|
||||
disallowedMessage: `import from @kbn/utility-types-jest instead`
|
||||
disallowedMessage: `import from @kbn/utility-types-jest instead`,
|
||||
},
|
||||
{
|
||||
from: '@kbn/inspector-plugin',
|
||||
|
@ -149,142 +144,126 @@ module.exports = {
|
|||
* of the file being linted so that we could re-route imports from `plugin-client` types to a different package
|
||||
* than `plugin-server` types.
|
||||
*/
|
||||
'@kbn/imports/exports_moved_packages': ['error', [
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/tooling-log',
|
||||
exportNames: [
|
||||
'DEFAULT_LOG_LEVEL',
|
||||
'getLogLevelFlagsHelp',
|
||||
'LOG_LEVEL_FLAGS',
|
||||
'LogLevel',
|
||||
'Message',
|
||||
'ParsedLogLevel',
|
||||
'parseLogLevel',
|
||||
'pickLevelFromFlags',
|
||||
'ToolingLog',
|
||||
'ToolingLogCollectingWriter',
|
||||
'ToolingLogOptions',
|
||||
'ToolingLogTextWriter',
|
||||
'ToolingLogTextWriterConfig',
|
||||
'Writer',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/ci-stats-reporter',
|
||||
exportNames: [
|
||||
'CiStatsMetric',
|
||||
'CiStatsReporter',
|
||||
'CiStatsReportTestsOptions',
|
||||
'CiStatsTestGroupInfo',
|
||||
'CiStatsTestResult',
|
||||
'CiStatsTestRun',
|
||||
'CiStatsTestType',
|
||||
'CiStatsTiming',
|
||||
'getTimeReporter',
|
||||
'MetricsOptions',
|
||||
'TimingsOptions',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/ci-stats-core',
|
||||
exportNames: [
|
||||
'Config',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/jest-serializers',
|
||||
exportNames: [
|
||||
'createAbsolutePathSerializer',
|
||||
'createStripAnsiSerializer',
|
||||
'createRecursiveSerializer',
|
||||
'createAnyInstanceSerializer',
|
||||
'createReplaceSerializer',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/stdio-dev-helpers',
|
||||
exportNames: [
|
||||
'observeReadable',
|
||||
'observeLines',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/sort-package-json',
|
||||
exportNames: [
|
||||
'sortPackageJson',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/dev-cli-runner',
|
||||
exportNames: [
|
||||
'run',
|
||||
'Command',
|
||||
'RunWithCommands',
|
||||
'CleanupTask',
|
||||
'Command',
|
||||
'CommandRunFn',
|
||||
'FlagOptions',
|
||||
'Flags',
|
||||
'RunContext',
|
||||
'RunFn',
|
||||
'RunOptions',
|
||||
'RunWithCommands',
|
||||
'RunWithCommandsOptions',
|
||||
'getFlags',
|
||||
'mergeFlagOptions'
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/dev-cli-errors',
|
||||
exportNames: [
|
||||
'createFailError',
|
||||
'createFlagError',
|
||||
'isFailError',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/dev-proc-runner',
|
||||
exportNames: [
|
||||
'withProcRunner',
|
||||
'ProcRunner',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/utils',
|
||||
to: '@kbn/repo-info',
|
||||
exportNames: [
|
||||
'REPO_ROOT',
|
||||
'UPSTREAM_BRANCH',
|
||||
'kibanaPackageJson',
|
||||
'isKibanaDistributable',
|
||||
'fromRoot',
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/presentation-util-plugin/common',
|
||||
to: '@kbn/presentation-util-plugin/test_helpers',
|
||||
exportNames: [
|
||||
'functionWrapper',
|
||||
'fontStyle'
|
||||
]
|
||||
},
|
||||
{
|
||||
from: '@kbn/fleet-plugin/common',
|
||||
to: '@kbn/fleet-plugin/common/mocks',
|
||||
exportNames: [
|
||||
'createFleetAuthzMock'
|
||||
]
|
||||
}
|
||||
]],
|
||||
'@kbn/imports/exports_moved_packages': [
|
||||
'error',
|
||||
[
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/tooling-log',
|
||||
exportNames: [
|
||||
'DEFAULT_LOG_LEVEL',
|
||||
'getLogLevelFlagsHelp',
|
||||
'LOG_LEVEL_FLAGS',
|
||||
'LogLevel',
|
||||
'Message',
|
||||
'ParsedLogLevel',
|
||||
'parseLogLevel',
|
||||
'pickLevelFromFlags',
|
||||
'ToolingLog',
|
||||
'ToolingLogCollectingWriter',
|
||||
'ToolingLogOptions',
|
||||
'ToolingLogTextWriter',
|
||||
'ToolingLogTextWriterConfig',
|
||||
'Writer',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/ci-stats-reporter',
|
||||
exportNames: [
|
||||
'CiStatsMetric',
|
||||
'CiStatsReporter',
|
||||
'CiStatsReportTestsOptions',
|
||||
'CiStatsTestGroupInfo',
|
||||
'CiStatsTestResult',
|
||||
'CiStatsTestRun',
|
||||
'CiStatsTestType',
|
||||
'CiStatsTiming',
|
||||
'getTimeReporter',
|
||||
'MetricsOptions',
|
||||
'TimingsOptions',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/ci-stats-core',
|
||||
exportNames: ['Config'],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/jest-serializers',
|
||||
exportNames: [
|
||||
'createAbsolutePathSerializer',
|
||||
'createStripAnsiSerializer',
|
||||
'createRecursiveSerializer',
|
||||
'createAnyInstanceSerializer',
|
||||
'createReplaceSerializer',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/stdio-dev-helpers',
|
||||
exportNames: ['observeReadable', 'observeLines'],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/sort-package-json',
|
||||
exportNames: ['sortPackageJson'],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/dev-cli-runner',
|
||||
exportNames: [
|
||||
'run',
|
||||
'Command',
|
||||
'RunWithCommands',
|
||||
'CleanupTask',
|
||||
'Command',
|
||||
'CommandRunFn',
|
||||
'FlagOptions',
|
||||
'Flags',
|
||||
'RunContext',
|
||||
'RunFn',
|
||||
'RunOptions',
|
||||
'RunWithCommands',
|
||||
'RunWithCommandsOptions',
|
||||
'getFlags',
|
||||
'mergeFlagOptions',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/dev-cli-errors',
|
||||
exportNames: ['createFailError', 'createFlagError', 'isFailError'],
|
||||
},
|
||||
{
|
||||
from: '@kbn/dev-utils',
|
||||
to: '@kbn/dev-proc-runner',
|
||||
exportNames: ['withProcRunner', 'ProcRunner'],
|
||||
},
|
||||
{
|
||||
from: '@kbn/utils',
|
||||
to: '@kbn/repo-info',
|
||||
exportNames: [
|
||||
'REPO_ROOT',
|
||||
'UPSTREAM_BRANCH',
|
||||
'kibanaPackageJson',
|
||||
'isKibanaDistributable',
|
||||
'fromRoot',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: '@kbn/presentation-util-plugin/common',
|
||||
to: '@kbn/presentation-util-plugin/test_helpers',
|
||||
exportNames: ['functionWrapper', 'fontStyle'],
|
||||
},
|
||||
{
|
||||
from: '@kbn/fleet-plugin/common',
|
||||
to: '@kbn/fleet-plugin/common/mocks',
|
||||
exportNames: ['createFleetAuthzMock'],
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
'@kbn/disable/no_protected_eslint_disable': 'error',
|
||||
'@kbn/disable/no_naked_eslint_disable': 'error',
|
||||
|
|
13
packages/kbn-eslint-plugin-telemetry/README.mdx
Normal file
13
packages/kbn-eslint-plugin-telemetry/README.mdx
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
id: kibDevDocsOpsEslintPluginTelemetry
|
||||
slug: /kibana-dev-docs/ops/kbn-eslint-plugin-telemetry
|
||||
title: '@kbn/eslint-plugin-telemetry'
|
||||
description: Custom ESLint rules to support telemetry in the Kibana repository
|
||||
tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'telemetry']
|
||||
---
|
||||
|
||||
`@kbn/eslint-plugin-telemetry` is an ESLint plugin providing custom rules for validating JSXCode in the Kibana repo to make sure it can be instrumented for the purposes of telemetry.
|
||||
|
||||
## `@kbn/telemetry/instrumentable_elements_should_be_instrumented`
|
||||
|
||||
This rule warns engineers to add `data-test-subj` to instrumentable components. It currently suggests the most widely used EUI components (`EuiButton`, `EuiFieldText`, etc), but can be expanded with often-used specific components used in the Kibana repo.
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { Scope } from 'eslint';
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree';
|
||||
|
||||
export function checkNodeForExistingDataTestSubjProp(
|
||||
node: TSESTree.JSXOpeningElement,
|
||||
getScope: () => Scope.Scope
|
||||
): boolean {
|
||||
const hasJsxDataTestSubjProp = node.attributes.find(
|
||||
(attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === 'data-test-subj'
|
||||
);
|
||||
|
||||
if (hasJsxDataTestSubjProp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const spreadedVariable = node.attributes.find(
|
||||
(attr) => attr.type === AST_NODE_TYPES.JSXSpreadAttribute
|
||||
);
|
||||
|
||||
if (
|
||||
!spreadedVariable ||
|
||||
!('argument' in spreadedVariable) ||
|
||||
!('name' in spreadedVariable.argument)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { name } = spreadedVariable.argument; // The name of the spreaded variable
|
||||
|
||||
const variable = getScope().variables.find((v) => v.name === name); // the variable definition of the spreaded variable
|
||||
|
||||
return variable && variable.defs.length > 0
|
||||
? variable.defs[0].node.init.properties.find((property: TSESTree.Property) => {
|
||||
if ('value' in property.key) {
|
||||
return property.key.value === 'data-test-subj';
|
||||
}
|
||||
return false;
|
||||
})
|
||||
: false;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getAppName } from './get_app_name';
|
||||
|
||||
const SYSTEMPATH = 'systemPath';
|
||||
|
||||
const testMap = [
|
||||
['x-pack/plugins/observability/foo/bar/baz/header_actions.tsx', 'o11y'],
|
||||
['x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx', 'apm'],
|
||||
['x-pack/plugins/cases/public/components/foo.tsx', 'cases'],
|
||||
['packages/kbn-alerts-ui-shared/src/alert_lifecycle_status_badge/index.tsx', 'kbnAlertsUiShared'],
|
||||
];
|
||||
|
||||
describe('Get App Name', () => {
|
||||
test.each(testMap)(
|
||||
'should get the responsible app name from a file path',
|
||||
(path, expectedValue) => {
|
||||
const appName = getAppName(`${SYSTEMPATH}/${path}`, SYSTEMPATH);
|
||||
expect(appName).toBe(expectedValue);
|
||||
}
|
||||
);
|
||||
});
|
49
packages/kbn-eslint-plugin-telemetry/helpers/get_app_name.ts
Normal file
49
packages/kbn-eslint-plugin-telemetry/helpers/get_app_name.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { camelCase } from 'lodash';
|
||||
import path from 'path';
|
||||
import { getPkgDirMap } from '@kbn/repo-packages';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
|
||||
export function getAppName(fileName: string, cwd: string) {
|
||||
const { dir } = path.parse(fileName);
|
||||
const relativePathToFile = dir.replace(cwd, '');
|
||||
|
||||
const packageDirs = Array.from(
|
||||
Array.from(getPkgDirMap(REPO_ROOT).values()).reduce((acc, currentDir) => {
|
||||
const topDirectory = currentDir.normalizedRepoRelativeDir.split('/')[0];
|
||||
|
||||
if (topDirectory) {
|
||||
acc.add(topDirectory);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Set<string>())
|
||||
);
|
||||
|
||||
const relativePathArray = relativePathToFile.split('/');
|
||||
|
||||
const appName = camelCase(
|
||||
packageDirs.reduce((acc, repoPath) => {
|
||||
if (!relativePathArray[1]) return '';
|
||||
|
||||
if (relativePathArray[1] === 'x-pack') {
|
||||
return relativePathArray[3];
|
||||
}
|
||||
|
||||
if (relativePathArray[1].includes(repoPath)) {
|
||||
return relativePathArray[2];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, '')
|
||||
);
|
||||
|
||||
return appName === 'observability' ? 'o11y' : appName;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree';
|
||||
|
||||
export function getFunctionName(func: TSESTree.FunctionDeclaration | TSESTree.Node): string {
|
||||
if (
|
||||
'id' in func &&
|
||||
func.id &&
|
||||
func.type === AST_NODE_TYPES.FunctionDeclaration &&
|
||||
func.id.type === AST_NODE_TYPES.Identifier
|
||||
) {
|
||||
return func.id.name;
|
||||
}
|
||||
|
||||
if (
|
||||
func.parent &&
|
||||
(func.parent.type !== AST_NODE_TYPES.VariableDeclarator ||
|
||||
func.parent.id.type !== AST_NODE_TYPES.Identifier)
|
||||
) {
|
||||
return getFunctionName(func.parent);
|
||||
}
|
||||
|
||||
if (func.parent?.id && 'name' in func.parent.id) {
|
||||
return func.parent.id.name;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
|
||||
/*
|
||||
Attempts to get a string representation of the intent
|
||||
out of an array of nodes.
|
||||
|
||||
Currently supported node types in the array:
|
||||
* String literal text (JSXText)
|
||||
* Translated text via <FormattedMessage> component -> uses prop `defaultMessage`
|
||||
* Translated text via {i18n.translate} call -> uses passed options object key `defaultMessage`
|
||||
*/
|
||||
export function getIntentFromNode(originalNode: TSESTree.JSXOpeningElement): string {
|
||||
const parent = originalNode.parent as TSESTree.JSXElement;
|
||||
|
||||
const node = Array.isArray(parent.children) ? parent.children : [];
|
||||
|
||||
if (node.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/*
|
||||
In order to satisfy TS we need to do quite a bit of defensive programming.
|
||||
This is my best attempt at providing the minimum amount of typeguards and
|
||||
keeping the code readable. In the cases where types are explicitly set to
|
||||
variables, it was done to help the compiler when it couldn't infer the type.
|
||||
*/
|
||||
return node.reduce((acc: string, currentNode) => {
|
||||
switch (currentNode.type) {
|
||||
case 'JSXText':
|
||||
// When node is a string primitive
|
||||
return `${acc}${strip(currentNode.value)}`;
|
||||
|
||||
case 'JSXElement':
|
||||
// Determining whether node is of form `<FormattedMessage defaultMessage="message" />`
|
||||
const name: TSESTree.JSXTagNameExpression = currentNode.openingElement.name;
|
||||
const attributes: Array<TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute> =
|
||||
currentNode.openingElement.attributes;
|
||||
|
||||
if (!('name' in name) || name.name !== 'FormattedMessage') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const defaultMessageProp = attributes.find(
|
||||
(attribute) => 'name' in attribute && attribute.name.name === 'defaultMessage'
|
||||
);
|
||||
|
||||
if (
|
||||
!defaultMessageProp ||
|
||||
!('value' in defaultMessageProp) ||
|
||||
!('type' in defaultMessageProp.value!) ||
|
||||
defaultMessageProp.value.type !== 'Literal' ||
|
||||
typeof defaultMessageProp.value.value !== 'string'
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${acc}${strip(defaultMessageProp.value.value)}`;
|
||||
|
||||
case 'JSXExpressionContainer':
|
||||
// Determining whether node is of form `{i18n.translate('foo', { defaultMessage: 'message'})}`
|
||||
const expression: TSESTree.JSXEmptyExpression | TSESTree.Expression =
|
||||
currentNode.expression;
|
||||
|
||||
if (!('arguments' in expression)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const args: TSESTree.CallExpressionArgument[] = expression.arguments;
|
||||
const callee: TSESTree.LeftHandSideExpression = expression.callee;
|
||||
|
||||
if (!('object' in callee)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const object: TSESTree.LeftHandSideExpression = callee.object;
|
||||
const property: TSESTree.Expression | TSESTree.PrivateIdentifier = callee.property;
|
||||
|
||||
if (!('name' in object) || !('name' in property)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (object.name !== 'i18n' || property.name !== 'translate') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const callExpressionArgument: TSESTree.CallExpressionArgument | undefined = args.find(
|
||||
(arg) => arg.type === 'ObjectExpression'
|
||||
);
|
||||
|
||||
if (!callExpressionArgument || callExpressionArgument.type !== 'ObjectExpression') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const defaultMessageValue: TSESTree.ObjectLiteralElement | undefined =
|
||||
callExpressionArgument.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'Property' && 'name' in prop.key && prop.key.name === 'defaultMessage'
|
||||
);
|
||||
|
||||
if (
|
||||
!defaultMessageValue ||
|
||||
!('value' in defaultMessageValue) ||
|
||||
defaultMessageValue.value.type !== 'Literal' ||
|
||||
typeof defaultMessageValue.value.value !== 'string'
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${acc}${strip(defaultMessageValue.value.value)}`;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, '');
|
||||
}
|
||||
|
||||
function strip(input: string): string {
|
||||
if (!input) return '';
|
||||
|
||||
const cleanedString = camelCase(input);
|
||||
|
||||
return `${cleanedString.charAt(0).toUpperCase()}${cleanedString.slice(1)}`;
|
||||
}
|
17
packages/kbn-eslint-plugin-telemetry/index.ts
Normal file
17
packages/kbn-eslint-plugin-telemetry/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EventGeneratingElementsShouldBeInstrumented } from './rules/event_generating_elements_should_be_instrumented';
|
||||
|
||||
/**
|
||||
* Custom ESLint rules, add `'@kbn/eslint-plugin-telemetry'` to your eslint config to use them
|
||||
* @internal
|
||||
*/
|
||||
export const rules = {
|
||||
event_generating_elements_should_be_instrumented: EventGeneratingElementsShouldBeInstrumented,
|
||||
};
|
13
packages/kbn-eslint-plugin-telemetry/jest.config.js
Normal file
13
packages/kbn-eslint-plugin-telemetry/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-eslint-plugin-telemetry'],
|
||||
};
|
6
packages/kbn-eslint-plugin-telemetry/kibana.jsonc
Normal file
6
packages/kbn-eslint-plugin-telemetry/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/eslint-plugin-telemetry",
|
||||
"owner": "@elastic/actionable-observability",
|
||||
"devOnly": true
|
||||
}
|
6
packages/kbn-eslint-plugin-telemetry/package.json
Normal file
6
packages/kbn-eslint-plugin-telemetry/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/eslint-plugin-telemetry",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import {
|
||||
EventGeneratingElementsShouldBeInstrumented,
|
||||
EVENT_GENERATING_ELEMENTS,
|
||||
} from './event_generating_elements_should_be_instrumented';
|
||||
|
||||
const tsTester = [
|
||||
'@typescript-eslint/parser',
|
||||
new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2018,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
] as const;
|
||||
|
||||
const babelTester = [
|
||||
'@babel/eslint-parser',
|
||||
new RuleTester({
|
||||
parser: require.resolve('@babel/eslint-parser'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2018,
|
||||
requireConfigFile: false,
|
||||
babelOptions: {
|
||||
presets: ['@kbn/babel-preset/node_preset'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
] as const;
|
||||
|
||||
for (const [name, tester] of [tsTester, babelTester]) {
|
||||
describe(name, () => {
|
||||
tester.run(
|
||||
'@kbn/event_generating_elements_should_be_instrumented',
|
||||
EventGeneratingElementsShouldBeInstrumented,
|
||||
{
|
||||
valid: EVENT_GENERATING_ELEMENTS.map((element) => ({
|
||||
filename: 'foo.tsx',
|
||||
code: `<${element} data-test-subj="foo" />`,
|
||||
})),
|
||||
|
||||
invalid: EVENT_GENERATING_ELEMENTS.map((element) => ({
|
||||
filename: 'foo.tsx',
|
||||
code: `<${element}>Value</${element}>`,
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message: `<${element}> should have a \`data-test-subj\` for telemetry purposes. Use the autofix suggestion or add your own.`,
|
||||
},
|
||||
],
|
||||
output: `<${element} data-test-subj="Value${element
|
||||
.replace('Eui', '')
|
||||
.replace('Empty', '')}">Value</${element}>`,
|
||||
})),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Rule } from 'eslint';
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree';
|
||||
|
||||
import { checkNodeForExistingDataTestSubjProp } from '../helpers/check_node_for_existing_data_test_subj_prop';
|
||||
import { getIntentFromNode } from '../helpers/get_intent_from_node';
|
||||
import { getAppName } from '../helpers/get_app_name';
|
||||
import { getFunctionName } from '../helpers/get_function_name';
|
||||
|
||||
export const EVENT_GENERATING_ELEMENTS = [
|
||||
'EuiButton',
|
||||
'EuiButtonEmpty',
|
||||
'EuiLink',
|
||||
'EuiFieldText',
|
||||
'EuiFieldSearch',
|
||||
'EuiFieldNumber',
|
||||
'EuiSelect',
|
||||
'EuiRadioGroup',
|
||||
'EuiTextArea',
|
||||
];
|
||||
|
||||
export const EventGeneratingElementsShouldBeInstrumented: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
const { getCwd, getFilename, getScope, report } = context;
|
||||
|
||||
return {
|
||||
JSXIdentifier: (node: TSESTree.Node) => {
|
||||
if (!('name' in node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = String(node.name);
|
||||
const range = node.range;
|
||||
const parent = node.parent;
|
||||
|
||||
if (
|
||||
parent?.type !== AST_NODE_TYPES.JSXOpeningElement ||
|
||||
!EVENT_GENERATING_ELEMENTS.includes(name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDataTestSubjProp = checkNodeForExistingDataTestSubjProp(parent, getScope);
|
||||
|
||||
if (hasDataTestSubjProp) {
|
||||
// JSXOpeningElement already has a prop for data-test-subj. Bail.
|
||||
return;
|
||||
}
|
||||
|
||||
// Start building the suggestion.
|
||||
|
||||
// 1. The app name
|
||||
const cwd = getCwd();
|
||||
const fileName = getFilename();
|
||||
const appName = getAppName(fileName, cwd);
|
||||
|
||||
// 2. Component name
|
||||
const functionDeclaration = getScope().block as TSESTree.FunctionDeclaration;
|
||||
const functionName = getFunctionName(functionDeclaration);
|
||||
const componentName = `${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`;
|
||||
|
||||
// 3. The intention of the element (i.e. "Select date", "Submit", "Cancel")
|
||||
const intent = getIntentFromNode(parent);
|
||||
|
||||
// 4. The element name that generates the events
|
||||
const element = name.replace('Eui', '').replace('Empty', '');
|
||||
|
||||
const suggestion = `${appName}${componentName}${intent}${element}`; // 'o11yHeaderActionsSubmitButton'
|
||||
|
||||
// 6. Report feedback to engineer
|
||||
report({
|
||||
node: node as any,
|
||||
message: `<${name}> should have a \`data-test-subj\` for telemetry purposes. Use the autofix suggestion or add your own.`,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfterRange(range, ` data-test-subj="${suggestion}"`);
|
||||
},
|
||||
});
|
||||
},
|
||||
} as Rule.RuleListener;
|
||||
},
|
||||
};
|
11
packages/kbn-eslint-plugin-telemetry/tsconfig.json
Normal file
11
packages/kbn-eslint-plugin-telemetry/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": ["jest", "node"],
|
||||
"lib": ["es2021"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["target/**/*"],
|
||||
"kbn_references": ["@kbn/repo-packages", "@kbn/repo-info"]
|
||||
}
|
|
@ -662,6 +662,8 @@
|
|||
"@kbn/eslint-plugin-eslint/*": ["packages/kbn-eslint-plugin-eslint/*"],
|
||||
"@kbn/eslint-plugin-imports": ["packages/kbn-eslint-plugin-imports"],
|
||||
"@kbn/eslint-plugin-imports/*": ["packages/kbn-eslint-plugin-imports/*"],
|
||||
"@kbn/eslint-plugin-telemetry": ["packages/kbn-eslint-plugin-telemetry"],
|
||||
"@kbn/eslint-plugin-telemetry/*": ["packages/kbn-eslint-plugin-telemetry/*"],
|
||||
"@kbn/eso-plugin": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin"],
|
||||
"@kbn/eso-plugin/*": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/*"],
|
||||
"@kbn/event-annotation-plugin": ["src/plugins/event_annotation"],
|
||||
|
|
|
@ -171,6 +171,7 @@ export function TransactionDurationRuleType(props: Props) {
|
|||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
data-test-subj="apmTransactionDurationRuleTypeSelect"
|
||||
value={params.aggregationType}
|
||||
options={map(TRANSACTION_ALERT_AGGREGATION_TYPES, (label, key) => {
|
||||
return {
|
||||
|
|
|
@ -156,6 +156,7 @@ export function IsAboveField({
|
|||
})}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="apmIsAboveFieldFieldNumber"
|
||||
min={0}
|
||||
value={value ?? 0}
|
||||
onChange={(e) => onChange(parseInt(e.target.value, 10))}
|
||||
|
|
|
@ -56,7 +56,11 @@ export function CorrelationsProgressControls({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{!isRunning && (
|
||||
<EuiButton size="s" onClick={onRefresh}>
|
||||
<EuiButton
|
||||
data-test-subj="apmCorrelationsProgressControlsRefreshButton"
|
||||
size="s"
|
||||
onClick={onRefresh}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.correlations.refreshButtonTitle"
|
||||
defaultMessage="Refresh"
|
||||
|
@ -64,7 +68,11 @@ export function CorrelationsProgressControls({
|
|||
</EuiButton>
|
||||
)}
|
||||
{isRunning && (
|
||||
<EuiButton size="s" onClick={onCancel}>
|
||||
<EuiButton
|
||||
data-test-subj="apmCorrelationsProgressControlsCancelButton"
|
||||
size="s"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.correlations.cancelButtonTitle"
|
||||
defaultMessage="Cancel"
|
||||
|
|
|
@ -21,5 +21,9 @@ export function DependencyOperationDetailLink(query: Query) {
|
|||
query,
|
||||
});
|
||||
|
||||
return <EuiLink href={link}>{spanName}</EuiLink>;
|
||||
return (
|
||||
<EuiLink data-test-subj="apmDependencyOperationDetailLink" href={link}>
|
||||
{spanName}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export function DetailViewHeader({
|
|||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiLink href={backHref}>
|
||||
<EuiLink data-test-subj="apmDetailViewHeaderLink" href={backHref}>
|
||||
<EuiFlexGroup direction="row" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="arrowLeft" />
|
||||
|
|
|
@ -187,7 +187,10 @@ export function ErrorSampleDetails({
|
|||
</EuiFlexItem>
|
||||
{isTraceExplorerEnabled && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href={traceExplorerLink}>
|
||||
<EuiLink
|
||||
data-test-subj="apmErrorSampleDetailsLink"
|
||||
href={traceExplorerLink}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiIcon type="apmTrace" />
|
||||
|
|
|
@ -37,6 +37,7 @@ export function HelpPopoverButton({
|
|||
if (buttonTextEnabled) {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmHelpPopoverButtonButton"
|
||||
className="apmHelpPopover__buttonIcon"
|
||||
size="s"
|
||||
iconType="help"
|
||||
|
|
|
@ -98,7 +98,11 @@ export function ServerlessSummary({ serverlessId }: Props) {
|
|||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href="https://ela.st/feedback-aws-lambda" target="_blank">
|
||||
<EuiLink
|
||||
data-test-subj="apmServerlessSummaryGiveFeedbackLink"
|
||||
href="https://ela.st/feedback-aws-lambda"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate('xpack.apm.serverlessMetrics.summary.feedback', {
|
||||
defaultMessage: 'Give feedback',
|
||||
})}
|
||||
|
|
|
@ -127,7 +127,10 @@ export function ServiceNodeMetrics({ serviceNodeName }: Props) {
|
|||
defaultMessage="We could not identify which JVMs these metrics belong to. This is likely caused by running a version of APM Server that is older than 7.5. Upgrading to APM Server 7.5 or higher should resolve this issue. For more information on upgrading, see the {link}. As an alternative, you can use the Kibana Query bar to filter by hostname, container ID or other fields."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={docLinks.links.apm.upgrading}>
|
||||
<EuiLink
|
||||
data-test-subj="apmServiceNodeMetricsDocumentationOfApmServerLink"
|
||||
href={docLinks.links.apm.upgrading}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningDocumentationLink',
|
||||
{ defaultMessage: 'documentation of APM Server' }
|
||||
|
|
|
@ -133,6 +133,7 @@ export function MobileFilters() {
|
|||
style={isLarge ? {} : { width: '225px' }}
|
||||
>
|
||||
<EuiSelect
|
||||
data-test-subj="apmMobileFiltersSelect"
|
||||
fullWidth={isSmall}
|
||||
isLoading={status === FETCH_STATUS.LOADING}
|
||||
prepend={label}
|
||||
|
|
|
@ -133,7 +133,10 @@ export function MobileServiceOverview() {
|
|||
preview. You can help us improve the experience by giving feedback. {feedbackLink}."
|
||||
values={{
|
||||
feedbackLink: (
|
||||
<EuiLink href="https://ela.st/feedback-mobile-apm">
|
||||
<EuiLink
|
||||
data-test-subj="apmMobileServiceOverviewGiveFeedbackLink"
|
||||
href="https://ela.st/feedback-mobile-apm"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceOverview.mobileCallOutLink',
|
||||
{
|
||||
|
@ -320,7 +323,10 @@ export function MobileServiceOverview() {
|
|||
fixedHeight={true}
|
||||
showPerPageOptions={false}
|
||||
link={
|
||||
<EuiLink href={dependenciesLink}>
|
||||
<EuiLink
|
||||
data-test-subj="apmMobileServiceOverviewViewDependenciesLink"
|
||||
href={dependenciesLink}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceOverview.dependenciesTableTabLink',
|
||||
{ defaultMessage: 'View dependencies' }
|
||||
|
|
|
@ -32,6 +32,7 @@ export function EditButton({ onClick }: Props) {
|
|||
)}
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="apmEditButtonEditGroupButton"
|
||||
iconType="pencil"
|
||||
onClick={() => {
|
||||
dismissTour();
|
||||
|
|
|
@ -149,6 +149,7 @@ export function GroupDetails({
|
|||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="apmGroupDetailsFieldText"
|
||||
fullWidth
|
||||
value={description}
|
||||
onChange={(e) => {
|
||||
|
@ -181,7 +182,11 @@ export function GroupDetails({
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false} style={{ marginLeft: 'auto' }}>
|
||||
<EuiButtonEmpty onClick={onCloseModal} isDisabled={isLoading}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmGroupDetailsCancelButton"
|
||||
onClick={onCloseModal}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceGroups.groupDetailsForm.cancel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
|
@ -190,6 +195,7 @@ export function GroupDetails({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmGroupDetailsSelectServicesButton"
|
||||
fill
|
||||
iconType="sortRight"
|
||||
iconSide="right"
|
||||
|
|
|
@ -168,6 +168,7 @@ export function SelectServices({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmSelectServicesButton"
|
||||
onClick={() => {
|
||||
setKuery(stagedKuery);
|
||||
}}
|
||||
|
@ -244,6 +245,7 @@ export function SelectServices({
|
|||
<EuiFlexItem>
|
||||
<div>
|
||||
<EuiButton
|
||||
data-test-subj="apmSelectServicesEditGroupDetailsButton"
|
||||
color="text"
|
||||
onClick={onEditGroupDetailsClick}
|
||||
iconType="sortLeft"
|
||||
|
@ -257,7 +259,11 @@ export function SelectServices({
|
|||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onCloseModal} isDisabled={isLoading}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmSelectServicesCancelButton"
|
||||
onClick={onCloseModal}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceGroups.selectServicesForm.cancel',
|
||||
{
|
||||
|
@ -268,6 +274,7 @@ export function SelectServices({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmSelectServicesSaveGroupButton"
|
||||
fill
|
||||
onClick={() => {
|
||||
onSaveClick({ ...serviceGroup, kuery });
|
||||
|
|
|
@ -106,6 +106,7 @@ export function ServiceGroupsList() {
|
|||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="apmServiceGroupsListFieldText"
|
||||
icon="search"
|
||||
fullWidth
|
||||
value={filter}
|
||||
|
@ -176,6 +177,7 @@ export function ServiceGroupsList() {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="apmServiceGroupsListGiveFeedbackLink"
|
||||
href="https://ela.st/feedback-service-groups"
|
||||
target="_blank"
|
||||
>
|
||||
|
|
|
@ -35,6 +35,7 @@ const options: Array<{
|
|||
export function Sort({ type, onChange }: Props) {
|
||||
return (
|
||||
<EuiSelect
|
||||
data-test-subj="apmSortSelect"
|
||||
options={options}
|
||||
value={type}
|
||||
onChange={(e) => onChange(e.target.value as ServiceGroupsSortType)}
|
||||
|
|
|
@ -71,7 +71,12 @@ export function ServiceGroupsTour({
|
|||
title={title}
|
||||
anchorPosition={anchorPosition}
|
||||
footerAction={
|
||||
<EuiButtonEmpty color="text" size="xs" onClick={dismissTour}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmServiceGroupsTourDismissButton"
|
||||
color="text"
|
||||
size="xs"
|
||||
onClick={dismissTour}
|
||||
>
|
||||
{i18n.translate('xpack.apm.serviceGroups.tour.dismiss', {
|
||||
defaultMessage: 'Dismiss',
|
||||
})}
|
||||
|
|
|
@ -67,6 +67,7 @@ export const GenerateMap: Story<{}> = () => {
|
|||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
data-test-subj="apmGenerateMapGenerateServiceMapButton"
|
||||
onClick={() => {
|
||||
setElements(
|
||||
generateServiceMapElements({ size, hasAnomalies: true })
|
||||
|
@ -80,6 +81,7 @@ export const GenerateMap: Story<{}> = () => {
|
|||
<EuiFlexItem>
|
||||
<EuiToolTip position="right" content="Number of services">
|
||||
<EuiFieldNumber
|
||||
data-test-subj="apmGenerateMapFieldNumber"
|
||||
placeholder="Size"
|
||||
value={size}
|
||||
onChange={(e) => setSize(e.target.valueAsNumber)}
|
||||
|
@ -88,6 +90,7 @@ export const GenerateMap: Story<{}> = () => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
data-test-subj="apmGenerateMapGetJsonButton"
|
||||
onClick={() => {
|
||||
setJson(JSON.stringify({ elements }, null, 2));
|
||||
}}
|
||||
|
@ -183,6 +186,7 @@ export const MapFromJSON: Story<{}> = () => {
|
|||
/>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
data-test-subj="apmMapFromJSONRenderJsonButton"
|
||||
onClick={() => {
|
||||
updateRenderedElements();
|
||||
}}
|
||||
|
|
|
@ -68,7 +68,10 @@ export function EmptyBanner() {
|
|||
defaultMessage:
|
||||
"We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent.",
|
||||
})}{' '}
|
||||
<EuiLink href={docLinks.links.apm.supportedServiceMaps}>
|
||||
<EuiLink
|
||||
data-test-subj="apmEmptyBannerLearnMoreInTheDocsLink"
|
||||
href={docLinks.links.apm.supportedServiceMaps}
|
||||
>
|
||||
{i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', {
|
||||
defaultMessage: 'Learn more in the docs',
|
||||
})}
|
||||
|
|
|
@ -92,6 +92,7 @@ export function DependencyContents({
|
|||
<EuiFlexItem>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click*/}
|
||||
<EuiButton
|
||||
data-test-subj="apmDependencyContentsDependencyDetailsButton"
|
||||
href={detailsUrl}
|
||||
fill={true}
|
||||
onClick={() => {
|
||||
|
|
|
@ -67,6 +67,7 @@ export function EdgeContents({ elementData }: ContentsProps) {
|
|||
<EuiFlexItem>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click*/}
|
||||
<EuiButton
|
||||
data-test-subj="apmEdgeContentsExploreTracesButton"
|
||||
href={url}
|
||||
fill={true}
|
||||
onClick={() => {
|
||||
|
|
|
@ -129,14 +129,23 @@ export function ServiceContents({
|
|||
</EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexItem>
|
||||
<EuiButton href={detailsUrl} fill={true}>
|
||||
<EuiButton
|
||||
data-test-subj="apmServiceContentsServiceDetailsButton"
|
||||
href={detailsUrl}
|
||||
fill={true}
|
||||
>
|
||||
{i18n.translate('xpack.apm.serviceMap.serviceDetailsButtonText', {
|
||||
defaultMessage: 'Service Details',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton color="success" href={focusUrl} onClick={onFocusClick}>
|
||||
<EuiButton
|
||||
data-test-subj="apmServiceContentsFocusMapButton"
|
||||
color="success"
|
||||
href={focusUrl}
|
||||
onClick={onFocusClick}
|
||||
>
|
||||
{i18n.translate('xpack.apm.serviceMap.focusMapButtonText', {
|
||||
defaultMessage: 'Focus map',
|
||||
})}
|
||||
|
|
|
@ -46,7 +46,10 @@ export function TimeoutPrompt({
|
|||
function ApmSettingsDocLink() {
|
||||
const { docLinks } = useApmPluginContext().core;
|
||||
return (
|
||||
<EuiLink href={docLinks.links.apm.kibanaSettings}>
|
||||
<EuiLink
|
||||
data-test-subj="apmApmSettingsDocLinkLearnMoreAboutApmSettingsInTheDocsLink"
|
||||
href={docLinks.links.apm.kibanaSettings}
|
||||
>
|
||||
{i18n.translate('xpack.apm.serviceMap.timeoutPrompt.docsLink', {
|
||||
defaultMessage: 'Learn more about APM settings in the docs',
|
||||
})}
|
||||
|
|
|
@ -174,7 +174,10 @@ export function ServiceOverview() {
|
|||
fixedHeight={true}
|
||||
showPerPageOptions={false}
|
||||
link={
|
||||
<EuiLink href={dependenciesLink}>
|
||||
<EuiLink
|
||||
data-test-subj="apmServiceOverviewViewDependenciesLink"
|
||||
href={dependenciesLink}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceOverview.dependenciesTableTabLink',
|
||||
{ defaultMessage: 'View dependencies' }
|
||||
|
|
|
@ -188,7 +188,10 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
|
|||
{/* Cancel button */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<LegacyAPMLink path="/settings/agent-configuration">
|
||||
<EuiButtonEmpty color="primary">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmServicePageCancelButton"
|
||||
color="primary"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.agentConfig.servicePage.cancelButton',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
|
@ -200,6 +203,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
|
|||
{/* Next button */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmServicePageNextStepButton"
|
||||
type="submit"
|
||||
fill
|
||||
onClick={onClickNext}
|
||||
|
|
|
@ -41,6 +41,7 @@ function FormRow({
|
|||
case 'text': {
|
||||
return (
|
||||
<EuiFieldText
|
||||
data-test-subj="apmFormRowFieldText"
|
||||
placeholder={setting.placeholder}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(setting.key, e.target.value)}
|
||||
|
@ -51,6 +52,7 @@ function FormRow({
|
|||
case 'integer': {
|
||||
return (
|
||||
<EuiFieldNumber
|
||||
data-test-subj="apmFormRowFieldNumber"
|
||||
placeholder={setting.placeholder}
|
||||
value={(value as any) || ''}
|
||||
min={setting.min}
|
||||
|
@ -93,6 +95,7 @@ function FormRow({
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="apmFormRowFieldNumber"
|
||||
placeholder={setting.placeholder}
|
||||
value={amount}
|
||||
onChange={(e) =>
|
||||
|
|
|
@ -160,7 +160,11 @@ export function SettingsPage({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{!isEditMode && (
|
||||
<EuiButton onClick={onClickEdit} iconType="pencil">
|
||||
<EuiButton
|
||||
data-test-subj="apmSettingsPageEditButton"
|
||||
onClick={onClickEdit}
|
||||
iconType="pencil"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.agentConfig.chooseService.editButton',
|
||||
{ defaultMessage: 'Edit' }
|
||||
|
|
|
@ -96,6 +96,7 @@ function CreateConfigurationButton() {
|
|||
}
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="apmAgentConfigurationButtonCreateConfigurationButton"
|
||||
color="primary"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
|
|
|
@ -80,6 +80,7 @@ export function AgentConfigurationList({
|
|||
}
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="apmAgentConfigurationListCreateConfigurationButton"
|
||||
color="primary"
|
||||
fill
|
||||
href={createAgentConfigurationHref}
|
||||
|
@ -158,6 +159,7 @@ export function AgentConfigurationList({
|
|||
sortable: true,
|
||||
render: (_, config: Config) => (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmColumnsButton"
|
||||
flush="left"
|
||||
size="s"
|
||||
color="primary"
|
||||
|
|
|
@ -79,6 +79,7 @@ export function getInstanceColumns(
|
|||
values={{
|
||||
seeDocs: (
|
||||
<EuiLink
|
||||
data-test-subj="apmGetInstanceColumnsConfigurationOptionsLink"
|
||||
href={`${agentDocsPageUrl}${
|
||||
!isOpenTelemetryAgentName(agentName)
|
||||
? 'configuration.html#service-node-name'
|
||||
|
|
|
@ -133,6 +133,7 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
|
|||
error={formError}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="apmCreateAgentKeyFlyoutFieldText"
|
||||
name="name"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.namePlaceholder',
|
||||
|
@ -208,7 +209,10 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
|
|||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onCancel}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmCreateAgentKeyFlyoutCancelButton"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.cancelButton',
|
||||
{
|
||||
|
@ -219,6 +223,7 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmCreateAgentKeyFlyoutButton"
|
||||
fill={true}
|
||||
onClick={createAgentKey}
|
||||
type="submit"
|
||||
|
|
|
@ -44,6 +44,7 @@ export function AgentKeyCallOut({ name, token }: Props) {
|
|||
)}
|
||||
</p>
|
||||
<EuiFieldText
|
||||
data-test-subj="apmAgentKeyCallOutFieldText"
|
||||
readOnly
|
||||
value={token}
|
||||
aria-label={i18n.translate(
|
||||
|
|
|
@ -88,6 +88,7 @@ export function AgentKeys() {
|
|||
{areApiKeysEnabled && canManage && !isEmpty(agentKeys) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmAgentKeysCreateApmAgentKeyButton"
|
||||
onClick={() => setIsFlyoutVisible(true)}
|
||||
fill={true}
|
||||
iconType="plusInCircle"
|
||||
|
@ -238,6 +239,7 @@ function AgentKeysContent({
|
|||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="apmAgentKeysContentCreateApmAgentKeyButton"
|
||||
onClick={onCreateAgentClick}
|
||||
fill={true}
|
||||
iconType="plusInCircle"
|
||||
|
|
|
@ -36,6 +36,7 @@ export function ApiKeysNotEnabled() {
|
|||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
data-test-subj="apmApiKeysNotEnabledDocsLink"
|
||||
href={docLinks?.links.security.apiKeyServiceSettings}
|
||||
target="_blank"
|
||||
>
|
||||
|
|
|
@ -136,7 +136,11 @@ export function AddEnvironments({
|
|||
</EuiFormRow>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty aria-label="Cancel" onClick={onCancel}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmAddEnvironmentsCancelButton"
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText',
|
||||
{
|
||||
|
@ -147,6 +151,7 @@ export function AddEnvironments({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmAddEnvironmentsCreateJobsButton"
|
||||
isLoading={isSaving}
|
||||
isDisabled={isSaving || selectedOptions.length === 0}
|
||||
fill
|
||||
|
|
|
@ -231,7 +231,11 @@ export function JobsList({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton href={mlManageJobsHref} color="primary">
|
||||
<EuiButton
|
||||
data-test-subj="apmJobsListManageJobsButton"
|
||||
href={mlManageJobsHref}
|
||||
color="primary"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.anomalyDetection.jobList.manageMlJobsButtonText',
|
||||
{
|
||||
|
@ -241,7 +245,12 @@ export function JobsList({
|
|||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill iconType="plusInCircle" onClick={onAddEnvironments}>
|
||||
<EuiButton
|
||||
data-test-subj="apmJobsListCreateJobButton"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
onClick={onAddEnvironments}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.anomalyDetection.jobList.addEnvironments',
|
||||
{
|
||||
|
|
|
@ -242,6 +242,7 @@ export function ApmIndices() {
|
|||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="apmApmIndicesFieldText"
|
||||
disabled={!canSave}
|
||||
fullWidth
|
||||
name={configurationName}
|
||||
|
@ -255,7 +256,10 @@ export function ApmIndices() {
|
|||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={refetch}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmApmIndicesCancelButton"
|
||||
onClick={refetch}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.apmIndices.cancelButton',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
|
@ -276,6 +280,7 @@ export function ApmIndices() {
|
|||
}
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="apmApmIndicesApplyChangesButton"
|
||||
fill
|
||||
onClick={handleApplyChangesEvent}
|
||||
isLoading={isSaving}
|
||||
|
|
|
@ -53,7 +53,11 @@ export function BottomBarActions({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty color="ghost" onClick={onDiscardChanges}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmBottomBarActionsDiscardChangesButton"
|
||||
color="ghost"
|
||||
onClick={onDiscardChanges}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.bottomBarActions.discardChangesButton',
|
||||
{
|
||||
|
@ -64,6 +68,7 @@ export function BottomBarActions({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmBottomBarActionsButton"
|
||||
onClick={onSave}
|
||||
fill
|
||||
isLoading={isLoading}
|
||||
|
|
|
@ -25,6 +25,7 @@ export function DeleteButton({ onDelete, customLinkId }: Props) {
|
|||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmDeleteButtonDeleteButton"
|
||||
color="danger"
|
||||
isLoading={isDeleting}
|
||||
iconSide="right"
|
||||
|
|
|
@ -15,5 +15,12 @@ interface Props {
|
|||
|
||||
export function Documentation({ label }: Props) {
|
||||
const { docLinks } = useApmPluginContext().core;
|
||||
return <EuiLink href={docLinks.links.apm.customLinks}>{label}</EuiLink>;
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="apmCustomLinksDocumentationLink"
|
||||
href={docLinks.links.apm.customLinks}
|
||||
>
|
||||
{label}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -137,6 +137,7 @@ export function FiltersSection({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmCustomLinkFiltersSectionButton"
|
||||
iconType="trash"
|
||||
onClick={() => onRemoveFilter(idx)}
|
||||
disabled={!value && !key && filters.length === 1}
|
||||
|
@ -166,6 +167,7 @@ function AddFilterButton({
|
|||
}) {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmCustomLinkAddFilterButtonAddAnotherFilterButton"
|
||||
iconType="plusInCircle"
|
||||
onClick={onClick}
|
||||
disabled={isDisabled}
|
||||
|
|
|
@ -33,7 +33,12 @@ export function FlyoutFooter({
|
|||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmCustomLinkFlyoutFooterCloseButton"
|
||||
iconType="cross"
|
||||
onClick={onClose}
|
||||
flush="left"
|
||||
>
|
||||
{i18n.translate('xpack.apm.settings.customLink.flyout.close', {
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
|
@ -44,6 +49,7 @@ export function FlyoutFooter({
|
|||
<DeleteButton customLinkId={customLinkId} onDelete={onDelete} />
|
||||
)}
|
||||
<EuiButton
|
||||
data-test-subj="apmCustomLinkFlyoutFooterSaveButton"
|
||||
form="customLink_form"
|
||||
fill
|
||||
type="submit"
|
||||
|
|
|
@ -99,6 +99,7 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) {
|
|||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFieldSearch
|
||||
data-test-subj="apmCustomLinkTableFieldSearch"
|
||||
fullWidth
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={i18n.translate(
|
||||
|
|
|
@ -39,6 +39,7 @@ export function EmptyPrompt({
|
|||
values={{
|
||||
customLinkDocLinkText: (
|
||||
<EuiLink
|
||||
data-test-subj="apmCustomLinkEmptyPromptDocsLink"
|
||||
target="_blank"
|
||||
href={docLinks.links.apm.customLinks}
|
||||
>
|
||||
|
|
|
@ -84,6 +84,7 @@ export function GeneralSettings() {
|
|||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
data-test-subj="apmGeneralSettingsKibanaAdvancedSettingsLink"
|
||||
href={application.getUrlForApp('management', {
|
||||
path: `/kibana/settings?query=category:(observability)`,
|
||||
})}
|
||||
|
|
|
@ -17,7 +17,10 @@ export function CardFooterContent() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<EuiButton href={fleetCloudAgentPolicyHref}>
|
||||
<EuiButton
|
||||
data-test-subj="apmCardFooterContentViewTheApmIntegrationInFleetButton"
|
||||
href={fleetCloudAgentPolicyHref}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText',
|
||||
{ defaultMessage: 'View the APM integration in Fleet' }
|
||||
|
|
|
@ -35,7 +35,10 @@ export function UpgradeAvailableCard({
|
|||
defaultMessage="Even though your APM integration is setup, a new version of the APM integration is available for upgrade with your package policy. {upgradePackagePolicyLink} to get the most out of your setup."
|
||||
values={{
|
||||
upgradePackagePolicyLink: (
|
||||
<EuiLink href={upgradeApmPackagePolicyHref}>
|
||||
<EuiLink
|
||||
data-test-subj="apmUpgradeAvailableCardUpgradeYourApmIntegrationLink"
|
||||
href={upgradeApmPackagePolicyHref}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.schema.upgradeAvailable.upgradePackagePolicyLink',
|
||||
{ defaultMessage: 'Upgrade your APM integration' }
|
||||
|
|
|
@ -162,6 +162,7 @@ export function SchemaOverview({
|
|||
})}
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="apmSchemaOverviewSwitchToElasticAgentButton"
|
||||
fill
|
||||
isLoading={isLoadingConfirmation}
|
||||
isDisabled={isDisabled}
|
||||
|
@ -210,7 +211,11 @@ export function SchemaOverviewHeading() {
|
|||
</strong>
|
||||
),
|
||||
elasticAgentDocLink: (
|
||||
<EuiLink target="_blank" href={docLinks.links.apm.elasticAgent}>
|
||||
<EuiLink
|
||||
data-test-subj="apmSchemaOverviewHeadingElasticAgentLink"
|
||||
target="_blank"
|
||||
href={docLinks.links.apm.elasticAgent}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.schema.descriptionText.elasticAgentDocLinkText',
|
||||
{ defaultMessage: 'Elastic Agent' }
|
||||
|
|
|
@ -123,7 +123,10 @@ export function StorageExplorer() {
|
|||
defaultMessage="Enable progressive loading of data and optimized sorting for services list in {kibanaAdvancedSettingsLink}."
|
||||
values={{
|
||||
kibanaAdvancedSettingsLink: (
|
||||
<EuiLink href={getKibanaAdvancedSettingsHref(core)}>
|
||||
<EuiLink
|
||||
data-test-subj="apmStorageExplorerKibanaAdvancedSettingsLink"
|
||||
href={getKibanaAdvancedSettingsHref(core)}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.storageExplorer.longLoadingTimeCalloutLink',
|
||||
{
|
||||
|
@ -136,6 +139,7 @@ export function StorageExplorer() {
|
|||
/>
|
||||
</p>
|
||||
<EuiButton
|
||||
data-test-subj="apmStorageExplorerButton"
|
||||
onClick={() =>
|
||||
setCalloutDismissed({
|
||||
...calloutDismissed,
|
||||
|
@ -171,6 +175,7 @@ export function StorageExplorer() {
|
|||
)}
|
||||
</p>
|
||||
<EuiButton
|
||||
data-test-subj="apmStorageExplorerButton"
|
||||
onClick={() =>
|
||||
setCalloutDismissed({
|
||||
...calloutDismissed,
|
||||
|
|
|
@ -170,7 +170,11 @@ export function TipsAndResources() {
|
|||
title={title}
|
||||
description={description}
|
||||
footer={
|
||||
<EuiButton href={href} target="_blank">
|
||||
<EuiButton
|
||||
data-test-subj="apmTipsAndResourcesLearnMoreButton"
|
||||
href={href}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.storageExplorer.resources.learnMoreButton',
|
||||
{
|
||||
|
|
|
@ -169,7 +169,10 @@ export function StorageDetailsPerService({
|
|||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href={serviceOverviewLink}>
|
||||
<EuiLink
|
||||
data-test-subj="apmStorageDetailsPerServiceGoToServiceOverviewLink"
|
||||
href={serviceOverviewLink}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.storageExplorer.serviceDetails.serviceOverviewLink',
|
||||
{
|
||||
|
|
|
@ -186,7 +186,10 @@ export function SummaryStats() {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiLink href={serviceInventoryLink}>
|
||||
<EuiLink
|
||||
data-test-subj="apmSummaryStatsGoToServiceInventoryLink"
|
||||
href={serviceInventoryLink}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.storageExplorer.summary.serviceInventoryLink',
|
||||
{
|
||||
|
@ -196,7 +199,10 @@ export function SummaryStats() {
|
|||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLink href={getIndexManagementHref(core)}>
|
||||
<EuiLink
|
||||
data-test-subj="apmSummaryStatsGoToIndexManagementLink"
|
||||
href={getIndexManagementHref(core)}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.storageExplorer.summary.indexManagementLink',
|
||||
{
|
||||
|
|
|
@ -162,6 +162,7 @@ export function TraceSearchBox({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
data-test-subj="apmTraceSearchBoxSelect"
|
||||
id="select-query-language"
|
||||
value={query.type}
|
||||
onChange={(e) => {
|
||||
|
@ -189,6 +190,7 @@ export function TraceSearchBox({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmTraceSearchBoxSearchButton"
|
||||
isLoading={loading}
|
||||
onClick={() => {
|
||||
onQueryCommit();
|
||||
|
|
|
@ -25,6 +25,7 @@ function FullTraceButton({
|
|||
}) {
|
||||
return (
|
||||
<EuiButton
|
||||
data-test-subj="apmFullTraceButtonViewFullTraceButton"
|
||||
fill
|
||||
iconType="apmTrace"
|
||||
isLoading={isLoading}
|
||||
|
|
|
@ -129,6 +129,7 @@ export function Waterfall({
|
|||
<div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmWaterfallButton"
|
||||
style={{ zIndex: 3, position: 'absolute' }}
|
||||
iconType={isAccordionOpen ? 'fold' : 'unfold'}
|
||||
onClick={() => {
|
||||
|
|
|
@ -48,6 +48,7 @@ export function TruncateHeightSection({ children, previewHeight }: Props) {
|
|||
{showToggle ? (
|
||||
<ToggleButtonContainer>
|
||||
<EuiLink
|
||||
data-test-subj="apmTruncateHeightSectionLink"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
|
|
|
@ -33,7 +33,10 @@ export function DroppedSpansWarning({
|
|||
values: { dropped },
|
||||
}
|
||||
)}{' '}
|
||||
<EuiLink href={docLinks.links.apm.droppedTransactionSpans}>
|
||||
<EuiLink
|
||||
data-test-subj="apmDroppedSpansWarningLearnMoreAboutDroppedSpansLink"
|
||||
href={docLinks.links.apm.droppedTransactionSpans}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText',
|
||||
{
|
||||
|
|
|
@ -64,6 +64,7 @@ export function EditDiscoveryRule({
|
|||
}}
|
||||
>
|
||||
<EuiSelect
|
||||
data-test-subj="apmEditDiscoveryRuleSelect"
|
||||
options={operationTypes.map((item) => ({
|
||||
text: item.operation.label,
|
||||
value: item.operation.value,
|
||||
|
@ -145,6 +146,7 @@ export function EditDiscoveryRule({
|
|||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="apmEditDiscoveryRuleFieldText"
|
||||
fullWidth
|
||||
value={probe}
|
||||
onChange={(e) => onChangeProbe(e.target.value)}
|
||||
|
@ -156,10 +158,16 @@ export function EditDiscoveryRule({
|
|||
)}
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onCancel}>Cancel</EuiButtonEmpty>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmEditDiscoveryRuleCancelButton"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmEditDiscoveryRuleButton"
|
||||
onClick={onSubmit}
|
||||
fill
|
||||
disabled={type === DISCOVERY_RULE_TYPE_ALL ? false : probe === ''}
|
||||
|
|
|
@ -68,6 +68,7 @@ export function JavaAgentVersionInput({ isValid, version, onChange }: Props) {
|
|||
values={{
|
||||
versionLink: (
|
||||
<EuiLink
|
||||
data-test-subj="apmJavaAgentVersionInputVersionLink"
|
||||
href={`${services.docLinks?.ELASTIC_WEBSITE_URL}/guide/en/apm/agent/java/current/release-notes.html`}
|
||||
target="_blank"
|
||||
>
|
||||
|
|
|
@ -147,6 +147,7 @@ export function RuntimeAttachment({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apmRuntimeAttachmentAddRuleButton"
|
||||
iconType="plusInCircle"
|
||||
disabled={editDiscoveryRuleId !== null}
|
||||
onClick={onAddRule}
|
||||
|
|
|
@ -32,7 +32,11 @@ function StepComponent() {
|
|||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiButton fill href={installApmAgentLink}>
|
||||
<EuiButton
|
||||
data-test-subj="apmStepComponentInstallApmAgentButton"
|
||||
fill
|
||||
href={installApmAgentLink}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentButtonText',
|
||||
{ defaultMessage: 'Install APM Agent' }
|
||||
|
|
|
@ -77,7 +77,11 @@ export function getTailSamplingSettings(docsLinks?: string): SettingsRow[] {
|
|||
defaultMessage="Learn more about tail sampling policies in our {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={docsLinks} target="_blank">
|
||||
<EuiLink
|
||||
data-test-subj="apmGetTailSamplingSettingsDocsLink"
|
||||
href={docsLinks}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.fleet_integration.settings.tailSamplingDocsHelpTextLink',
|
||||
{
|
||||
|
|
|
@ -167,6 +167,7 @@ function AdvancedOptions({ children }: { children: React.ReactNode }) {
|
|||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmAdvancedOptionsAdvancedOptionsButton"
|
||||
iconType={isOpen ? 'arrowDown' : 'arrowRight'}
|
||||
onClick={() => {
|
||||
setIsOpen((state) => !state);
|
||||
|
|
|
@ -27,7 +27,11 @@ export function Labs() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty color="text" onClick={toggleFlyoutVisibility}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmLabsLabsButton"
|
||||
color="text"
|
||||
onClick={toggleFlyoutVisibility}
|
||||
>
|
||||
{i18n.translate('xpack.apm.labs', { defaultMessage: 'Labs' })}
|
||||
</EuiButtonEmpty>
|
||||
{isOpen && <LabsFlyout onClose={toggleFlyoutVisibility} />}
|
||||
|
|
|
@ -133,14 +133,22 @@ export function LabsFlyout({ onClose }: Props) {
|
|||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={handelCancel}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmLabsFlyoutCancelButton"
|
||||
onClick={handelCancel}
|
||||
>
|
||||
{i18n.translate('xpack.apm.labs.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill isLoading={isSaving} onClick={handleSave}>
|
||||
<EuiButton
|
||||
data-test-subj="apmLabsFlyoutReloadToApplyChangesButton"
|
||||
fill
|
||||
isLoading={isSaving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{i18n.translate('xpack.apm.labs.reload', {
|
||||
defaultMessage: 'Reload to apply changes',
|
||||
})}
|
||||
|
|
|
@ -53,7 +53,11 @@ export const storageExplorer = {
|
|||
</EuiFlexGroup>
|
||||
),
|
||||
rightSideItems: [
|
||||
<EuiLink href={getStorageExplorerFeedbackHref()} target="_blank">
|
||||
<EuiLink
|
||||
data-test-subj="apmGiveFeedbackLink"
|
||||
href={getStorageExplorerFeedbackHref()}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.views.storageExplorer.giveFeedback',
|
||||
{
|
||||
|
|
|
@ -85,7 +85,11 @@ export function AnalyzeDataButton() {
|
|||
'Explore Data allows you to select and filter result data in any dimension, and look for the cause or impact of performance problems',
|
||||
})}
|
||||
>
|
||||
<EuiButtonEmpty href={href} iconType="visBarVerticalStacked">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmAnalyzeDataButtonExploreDataButton"
|
||||
href={href}
|
||||
iconType="visBarVerticalStacked"
|
||||
>
|
||||
{i18n.translate('xpack.apm.analyzeDataButton.label', {
|
||||
defaultMessage: 'Explore data',
|
||||
})}
|
||||
|
|
|
@ -102,6 +102,7 @@ export function LatencyChart({ height, kuery }: Props) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
data-test-subj="apmLatencyChartSelect"
|
||||
compressed
|
||||
prepend={i18n.translate(
|
||||
'xpack.apm.serviceOverview.latencyChartTitle.prepend',
|
||||
|
|
|
@ -10,7 +10,10 @@ import { EuiLink } from '@elastic/eui';
|
|||
|
||||
export function DependenciesTableServiceMapLink({ href }: { href: string }) {
|
||||
return (
|
||||
<EuiLink href={href}>
|
||||
<EuiLink
|
||||
data-test-subj="apmDependenciesTableServiceMapLinkViewServiceMapLink"
|
||||
href={href}
|
||||
>
|
||||
{i18n.translate('xpack.apm.dependenciesTable.serviceMapLinkText', {
|
||||
defaultMessage: 'View service map',
|
||||
})}
|
||||
|
|
|
@ -49,7 +49,11 @@ export function LicensePrompt({
|
|||
titleElement="h2"
|
||||
description={<EuiTextColor color="subdued">{text}</EuiTextColor>}
|
||||
footer={
|
||||
<EuiButton fill={true} href={licensePageUrl}>
|
||||
<EuiButton
|
||||
data-test-subj="apmLicensePromptStartTrialButton"
|
||||
fill={true}
|
||||
href={licensePageUrl}
|
||||
>
|
||||
{i18n.translate('xpack.apm.license.button', {
|
||||
defaultMessage: 'Start trial',
|
||||
})}
|
||||
|
|
|
@ -100,5 +100,7 @@ export function LegacyAPMLink({
|
|||
|
||||
const href = getLegacyApmHref({ basePath, path, search, query: mergedQuery });
|
||||
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
return (
|
||||
<EuiLink data-test-subj="apmLegacyAPMLinkLink" {...rest} href={href} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,5 +27,11 @@ export function ErrorOverviewLink({ serviceName, query, ...rest }: Props) {
|
|||
query,
|
||||
});
|
||||
|
||||
return <EuiLink href={errorOverviewLink} {...rest} />;
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="apmErrorOverviewLinkLink"
|
||||
href={errorOverviewLink}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,5 +22,7 @@ interface ServiceMapLinkProps extends APMLinkExtendProps {
|
|||
|
||||
export function ServiceMapLink({ serviceName, ...rest }: ServiceMapLinkProps) {
|
||||
const href = useServiceMapHref(serviceName);
|
||||
return <EuiLink href={href} {...rest} />;
|
||||
return (
|
||||
<EuiLink data-test-subj="apmServiceMapLinkLink" href={href} {...rest} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,5 +46,11 @@ export function ServiceNodeMetricOverviewLink({
|
|||
serviceName,
|
||||
serviceNodeName,
|
||||
});
|
||||
return <EuiLink href={href} {...rest} />;
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="apmServiceNodeMetricOverviewLinkLink"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,5 +50,11 @@ export function ServiceOrTransactionsOverviewLink({
|
|||
environment,
|
||||
transactionType,
|
||||
});
|
||||
return <EuiLink href={href} {...rest} />;
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="apmServiceOrTransactionsOverviewLinkLink"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,7 +81,13 @@ export function TransactionDetailLink({
|
|||
return (
|
||||
<TruncateWithTooltip
|
||||
text={transactionName}
|
||||
content={<EuiLink href={href} {...rest} />}
|
||||
content={
|
||||
<EuiLink
|
||||
data-test-subj="apmTransactionDetailLinkLink"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,5 +48,11 @@ export function TransactionOverviewLink({
|
|||
latencyAggregationType,
|
||||
transactionType,
|
||||
});
|
||||
return <EuiLink href={href} {...rest} />;
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="apmTransactionOverviewLinkLink"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -69,5 +69,5 @@ export function DiscoverLink({ query = {}, ...rest }: Props) {
|
|||
location,
|
||||
});
|
||||
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
return <EuiLink data-test-subj="apmDiscoverLinkLink" {...rest} href={href} />;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ export function ElasticDocsLink({ section, path, children, ...rest }: Props) {
|
|||
return typeof children === 'function' ? (
|
||||
children(href)
|
||||
) : (
|
||||
<EuiLink href={href} {...rest}>
|
||||
<EuiLink data-test-subj="apmElasticDocsLinkLink" href={href} {...rest}>
|
||||
{children}
|
||||
</EuiLink>
|
||||
);
|
||||
|
|
|
@ -48,5 +48,5 @@ export const getInfraHref = ({
|
|||
export function InfraLink({ app, path, query = {}, ...rest }: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const href = getInfraHref({ app, basePath: core.http.basePath, query, path });
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
return <EuiLink data-test-subj="apmInfraLinkLink" {...rest} href={href} />;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export function MLExplorerLink({ jobId, external, children }: Props) {
|
|||
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="apmMLExplorerLinkLink"
|
||||
children={children}
|
||||
href={href}
|
||||
external={external}
|
||||
|
|
|
@ -22,6 +22,7 @@ export function MLManageJobsLink({ children, external, jobId }: Props) {
|
|||
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="apmMLManageJobsLinkLink"
|
||||
children={children}
|
||||
href={mlADLink}
|
||||
external={external}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue