Merge branch 'main' into ui-actions-refactor

This commit is contained in:
James Gowdy 2023-11-23 12:56:59 +00:00 committed by GitHub
commit bccf61bd4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 1441 additions and 447 deletions

View file

@ -941,7 +941,7 @@ module.exports = {
],
rules: {
'@kbn/i18n/strings_should_be_translated_with_i18n': 'warn',
'@kbn/i18n/strings_should_be_translated_with_formatted_message': 'warn',
'@kbn/i18n/i18n_translate_should_start_with_the_right_id': 'warn',
},
},
{

2
.github/CODEOWNERS vendored
View file

@ -368,7 +368,7 @@ src/plugins/es_ui_shared @elastic/platform-deployment-management
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-i18n @elastic/obs-knowledge-team
packages/kbn-eslint-plugin-i18n @elastic/obs-knowledge-team @elastic/kibana-operations
packages/kbn-eslint-plugin-imports @elastic/kibana-operations
packages/kbn-eslint-plugin-telemetry @elastic/obs-knowledge-team
x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security

View file

@ -6,22 +6,63 @@ description: Custom ESLint rules to support translations in the Kibana repositor
tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'i18n']
---
`@kbn/eslint-plugin-i18n` is an ESLint plugin providing custom rules for validating JSXCode in the Kibana repo to make sure they are translated.
# Summary
Note: At the moment these rules only work for apps that are inside `/x-pack/plugins`.
If you want to enable this rule on code that is outside of this path, adjust `/helpers/get_i18n_identifier_from_file_path.ts`.
`@kbn/eslint-plugin-i18n` is an ESLint plugin providing custom ESLint rules to help validating code in the Kibana repo in the area of translations.
The aim of this package is to help engineers type less and have a nicer experience.
If a rule does not behave as you expect or you have an idea of how these rules can be improved, please reach out to the Observability Knowledge Team or the Kibana Operations team.
# Rules
## `@kbn/i18n/strings_should_be_translated_with_i18n`
This rule warns engineers to translate their strings by using i18n.translate from the '@kbn/i18n' package. It provides an autofix that takes into account the context of the translatable string in the JSX tree to generate a translation ID.
It kicks in on JSXText elements and specific JSXAttributes (`label` and `aria-label`) which expect a translated value.
This rule warns engineers to translate their strings by using `i18n.translate` from the `@kbn/i18n` package.
## `@kbn/i18n/strings_should_be_translated_with_formatted_message`
It provides an autofix that takes into account the context of the translatable string in the JSX tree to generate a translation ID.
This rule warns engineers to translate their strings by using `<FormattedMessage>` from the '@kbn/i18n-react' package. It provides an autofix that takes into account the context of the translatable string in the JSX tree and to generate a translation ID.
It kicks in on JSXText elements and specific JSXAttributes (`label` and `aria-label`) which expect a translated value.
This rule kicks in on:
## Exemptions and exceptions
- JSXText elements;
- specific JSXAttributes (`label` and `aria-label`) which expect a translated value.
### Example
This code:
```
// Filename: /x-pack/plugins/observability/public/my_component.tsx
import React from 'react';
import { EuiText } from '@elastic/eui';
function MyComponent() {
return (
<EuiText>You know, for search</EuiText>
)
}
```
will be autofixed with:
```
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText } from '@elastic/eui';
function MyComponent() {
return (
<EuiText>
{i18n.translate('xpack.observability.myComponent.textLabel', { defaultMessage: 'You know, for search' } )}
</EuiText>
)
}
```
If `i18n` has not been imported yet, the autofix will automatically add the import statement as well.
### Exemptions and exceptions
A JSXText element or JSXAttribute `label` or `aria-label` of which the value is:
@ -32,3 +73,77 @@ A JSXText element or JSXAttribute `label` or `aria-label` of which the value is:
are exempt from this rule.
If this rule kicks in on a string value that you don't like, you can escape it by wrapping the string inside a JSXExpression: `{'my escaped value'}`.
---
## `@kbn/i18n/strings_should_be_translated_with_formatted_message`
This rule warns engineers to translate their strings by using `<FormattedMessage>` from the `@kbn/i18n-react` package.
It provides an autofix that takes into account the context of the translatable string in the JSX tree and to generate a translation ID.
This rule kicks in on:
- JSXText elements;
- specific JSXAttributes (`label` and `aria-label`) which expect a translated value.
### Exemptions and exceptions
A JSXText element or JSXAttribute `label` or `aria-label` of which the value is:
- wrapped in a `EuiCode` or `EuiBetaBadge` component,
- made up of non alpha characters such as `!@#$%^&*(){}` or numbers,
- wrapped in three backticks,
are exempt from this rule.
If this rule kicks in on a string value that you don't like, you can escape it by wrapping the string inside a JSXExpression: `{'my escaped value'}`.
---
## `@kbn/i18n/i18n_translate_should_start_with_the_right_id`
This rule checks every instance of `i18n.translate()` if the first parameter passed:
1. has a string value,
2. if the parameter starts with the correct i18n app identifier for the file.
It checks the repo for the `i18nrc.json` and `/x-pack/i18nrc.json` files and determines what the right i18n identifier should be.
If the parameter is missing or does not start with the right i18n identifier, it can autofix the parameter.
This rule is useful when defining translated values in plain functions (non-JSX), but it works in JSX as well.
### Example
This code:
```
// Filename: /x-pack/plugins/observability/public/my_function.ts
function myFunction() {
const translations = [
{
id: 'copy';
label: i18n.translate()
}
]
}
```
will be autofixed with:
```
import { i18n } from '@kbn/i18n';
function myFunction() {
const translations = [
{
id: 'copy';
label: i18n.translate('xpack.observability.myFunction.', { defaultMessage: '' })
}
]
}
```
If `i18n` has not been imported yet, the autofix will automatically add the import statement as well.

View file

@ -11,17 +11,15 @@ import { getI18nIdentifierFromFilePath } from './get_i18n_identifier_from_file_p
const SYSTEMPATH = 'systemPath';
const testMap = [
['x-pack/plugins/observability/foo/bar/baz/header_actions.tsx', 'xpack.observability'],
['x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx', 'xpack.apm'],
['x-pack/plugins/cases/public/components/foo.tsx', 'xpack.cases'],
['x-pack/plugins/observability/public/header_actions.tsx', 'xpack.observability'],
['x-pack/plugins/apm/common/components/app/correlations/correlations_table.tsx', 'xpack.apm'],
['x-pack/plugins/cases/server/components/foo.tsx', 'xpack.cases'],
[
'x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx',
'xpack.synthetics',
],
[
'packages/kbn-alerts-ui-shared/src/alert_lifecycle_status_badge/index.tsx',
'app_not_found_in_i18nrc',
],
['src/plugins/vis_types/gauge/public/editor/collections.ts', 'visTypeGauge'],
['packages/kbn-alerts-ui-shared/src/alert_lifecycle_status_badge/index.tsx', 'alertsUIShared'],
];
describe('Get i18n Identifier for file', () => {

View file

@ -14,18 +14,38 @@ export function getI18nIdentifierFromFilePath(fileName: string, cwd: string) {
const { dir } = parse(fileName);
const relativePathToFile = dir.replace(cwd, '');
const relativePathArray = relativePathToFile.split('/');
// We need to match the path of the file that is being worked in with the path
// that is noted in the values inside the i18nrc.json object.
// These values differ depending on which i18nrc.json object you look at (there are multiple)
// so we need to account for both notations.
const relativePathArray = relativePathToFile.includes('src')
? relativePathToFile.split('/').slice(1)
: relativePathToFile.split('/').slice(2);
const path = `${relativePathArray[2]}/${relativePathArray[3]}`;
const pluginNameIndex = relativePathArray.findIndex(
(el) => el === 'public' || el === 'server' || el === 'common'
);
const path = relativePathArray.slice(0, pluginNameIndex).join('/');
const xpackRC = resolve(join(__dirname, '../../../'), 'x-pack/.i18nrc.json');
const rootRC = resolve(join(__dirname, '../../../'), '.i18nrc.json');
const i18nrcFile = fs.readFileSync(xpackRC, 'utf8');
const i18nrc = JSON.parse(i18nrcFile);
const xpackI18nrcFile = fs.readFileSync(xpackRC, 'utf8');
const xpackI18nrc = JSON.parse(xpackI18nrcFile);
return i18nrc && i18nrc.paths
? findKey(i18nrc.paths, (v) =>
Array.isArray(v) ? v.find((e) => e === path) : typeof v === 'string' && v === path
) ?? 'app_not_found_in_i18nrc'
: 'could_not_find_i18nrc';
const rootI18nrcFile = fs.readFileSync(rootRC, 'utf8');
const rootI18nrc = JSON.parse(rootI18nrcFile);
const allPaths = { ...xpackI18nrc.paths, ...rootI18nrc.paths };
if (Object.keys(allPaths).length === 0) return 'could_not_find_i18nrc';
return (
findKey(allPaths, (value) =>
Array.isArray(value)
? value.find((el) => el === path)
: typeof value === 'string' && value === path
) ?? 'app_not_found_in_i18nrc'
);
}

View file

@ -10,10 +10,10 @@ import { SourceCode } from 'eslint';
export function getI18nImportFixer({
sourceCode,
mode,
translationFunction,
}: {
sourceCode: SourceCode;
mode: 'i18n.translate' | 'FormattedMessage';
translationFunction: 'i18n.translate' | 'FormattedMessage';
}) {
let existingI18nImportLineIndex = -1;
let i18nImportLineToBeAdded = '';
@ -27,7 +27,7 @@ export function getI18nImportFixer({
*
* */
if (mode === 'i18n.translate') {
if (translationFunction === 'i18n.translate') {
existingI18nImportLineIndex = sourceCode.lines.findIndex((l) => l.includes("from '@kbn/i18n'"));
const i18nImportLineInSource = sourceCode.lines[existingI18nImportLineIndex];
@ -46,7 +46,7 @@ export function getI18nImportFixer({
}
}
if (mode === 'FormattedMessage') {
if (translationFunction === 'FormattedMessage') {
existingI18nImportLineIndex = sourceCode.lines.findIndex((l) =>
l.includes("from '@kbn/i18n-react'")
);
@ -83,21 +83,27 @@ export function getI18nImportFixer({
return {
i18nImportLine: i18nImportLineToBeAdded,
rangeToAddI18nImportLine: [start, end] as [number, number],
mode: 'replace',
replaceMode: 'replace',
};
}
// If the file doesn't have an import line for the translation package yet, we need to add it.
// Pretty safe bet to add it underneath the import line for React.
const lineIndex = sourceCode.lines.findIndex((l) => l.includes("from 'react'"));
let lineIndex = sourceCode.lines.findIndex((l) => l.includes("from 'react'") || l.includes('*/'));
if (lineIndex === -1) {
lineIndex = 0;
}
const targetLine = sourceCode.lines[lineIndex];
// `getIndexFromLoc` is 0-based, so we need to add 1 to the line index.
const start = sourceCode.getIndexFromLoc({ line: lineIndex + 1, column: 0 });
const end = start + targetLine.length;
return {
i18nImportLine: i18nImportLineToBeAdded,
rangeToAddI18nImportLine: [start, end] as [number, number],
mode: 'insert',
replaceMode: 'insert',
};
}

View file

@ -8,6 +8,7 @@
import { StringsShouldBeTranslatedWithI18n } from './rules/strings_should_be_translated_with_i18n';
import { StringsShouldBeTranslatedWithFormattedMessage } from './rules/strings_should_be_translated_with_formatted_message';
import { I18nTranslateShouldStartWithTheRightId } from './rules/i18n_translate_should_start_with_the_right_id';
/**
* Custom ESLint rules, add `'@kbn/eslint-plugin-i18n'` to your eslint config to use them
@ -17,4 +18,5 @@ export const rules = {
strings_should_be_translated_with_i18n: StringsShouldBeTranslatedWithI18n,
strings_should_be_translated_with_formatted_message:
StringsShouldBeTranslatedWithFormattedMessage,
i18n_translate_should_start_with_the_right_id: I18nTranslateShouldStartWithTheRightId,
};

View file

@ -1,6 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/eslint-plugin-i18n",
"owner": "@elastic/obs-knowledge-team",
"owner": ["@elastic/obs-knowledge-team", "@elastic/kibana-operations"],
"devOnly": true
}

View file

@ -0,0 +1,134 @@
/*
* 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 {
I18nTranslateShouldStartWithTheRightId,
RULE_WARNING_MESSAGE,
} from './i18n_translate_should_start_with_the_right_id';
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;
const invalid: RuleTester.InvalidTestCase[] = [
{
name: 'When a string literal is passed to i18n.translate, it should start with the correct i18n identifier.',
filename: '/x-pack/plugins/observability/public/test_component.ts',
code: `
import { i18n } from '@kbn/i18n';
function TestComponent() {
const foo = i18n.translate('foo');
}`,
errors: [
{
line: 5,
message: RULE_WARNING_MESSAGE,
},
],
output: `
import { i18n } from '@kbn/i18n';
function TestComponent() {
const foo = i18n.translate('xpack.observability.testComponent.', { defaultMessage: '' });
}`,
},
{
name: 'When no string literal is passed to i18n.translate, it should start with the correct i18n identifier.',
filename: '/x-pack/plugins/observability/public/test_component.ts',
code: `
import { i18n } from '@kbn/i18n';
function TestComponent() {
const foo = i18n.translate();
}`,
errors: [
{
line: 5,
message: RULE_WARNING_MESSAGE,
},
],
output: `
import { i18n } from '@kbn/i18n';
function TestComponent() {
const foo = i18n.translate('xpack.observability.testComponent.', { defaultMessage: '' });
}`,
},
{
name: 'When i18n is not imported yet, the rule should add it.',
filename: '/x-pack/plugins/observability/public/test_component.ts',
code: `
function TestComponent() {
const foo = i18n.translate();
}`,
errors: [
{
line: 3,
message: RULE_WARNING_MESSAGE,
},
],
output: `
import { i18n } from '@kbn/i18n';
function TestComponent() {
const foo = i18n.translate('xpack.observability.testComponent.', { defaultMessage: '' });
}`,
},
];
const valid: RuleTester.ValidTestCase[] = [
{
name: invalid[0].name,
filename: invalid[0].filename,
code: invalid[0].output as string,
},
{
name: invalid[1].name,
filename: invalid[1].filename,
code: invalid[1].output as string,
},
];
for (const [name, tester] of [tsTester, babelTester]) {
describe(name, () => {
tester.run(
'@kbn/i18n_translate_should_start_with_the_right_id',
I18nTranslateShouldStartWithTheRightId,
{
valid,
invalid,
}
);
});
}

View file

@ -0,0 +1,82 @@
/*
* 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 { TSESTree } from '@typescript-eslint/typescript-estree';
import type { Rule } from 'eslint';
import { getI18nIdentifierFromFilePath } from '../helpers/get_i18n_identifier_from_file_path';
import { getFunctionName } from '../helpers/get_function_name';
import { getI18nImportFixer } from '../helpers/get_i18n_import_fixer';
import { isTruthy } from '../helpers/utils';
export const RULE_WARNING_MESSAGE =
'First parameter passed to i18n.translate should start with the correct i18n identifier for this file. Correct it or use the autofix suggestion.';
export const I18nTranslateShouldStartWithTheRightId: Rule.RuleModule = {
meta: {
type: 'suggestion',
fixable: 'code',
},
create(context) {
const { cwd, filename, getScope, sourceCode, report } = context;
return {
CallExpression: (node: TSESTree.CallExpression) => {
const { callee } = node;
if (
!callee ||
!('object' in callee) ||
!('property' in callee) ||
!('name' in callee.object) ||
!('name' in callee.property) ||
callee.object.name !== 'i18n' ||
callee.property.name !== 'translate'
)
return;
const identifier =
Array.isArray(node.arguments) &&
node.arguments.length &&
'value' in node.arguments[0] &&
typeof node.arguments[0].value === 'string' &&
node.arguments[0].value;
const i18nAppId = getI18nIdentifierFromFilePath(filename, cwd);
const functionDeclaration = getScope().block as TSESTree.FunctionDeclaration;
const functionName = getFunctionName(functionDeclaration);
// Check if i18n has already been imported into the file
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } =
getI18nImportFixer({
sourceCode,
translationFunction: 'i18n.translate',
});
if (!identifier || (identifier && !identifier.startsWith(`${i18nAppId}.`))) {
report({
node: node as any,
message: RULE_WARNING_MESSAGE,
fix(fixer) {
return [
fixer.replaceTextRange(
node.range,
`i18n.translate('${i18nAppId}.${functionName}.', { defaultMessage: '' })`
),
!hasI18nImportLine && rangeToAddI18nImportLine
? replaceMode === 'replace'
? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine)
: fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`)
: null,
].filter(isTruthy);
},
});
}
},
} as Rule.RuleListener;
},
};

View file

@ -7,7 +7,10 @@
*/
import { RuleTester } from 'eslint';
import { StringsShouldBeTranslatedWithFormattedMessage } from './strings_should_be_translated_with_formatted_message';
import {
StringsShouldBeTranslatedWithFormattedMessage,
RULE_WARNING_MESSAGE,
} from './strings_should_be_translated_with_formatted_message';
const tsTester = [
'@typescript-eslint/parser',
@ -41,7 +44,7 @@ const babelTester = [
const invalid: RuleTester.InvalidTestCase[] = [
{
name: 'A JSX element with a string literal should be translated with i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
@ -53,7 +56,7 @@ function TestComponent() {
errors: [
{
line: 6,
message: `Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -64,7 +67,7 @@ function TestComponent() {
return (
<div>
<FormattedMessage
id="app_not_found_in_i18nrc.testComponent.div.thisIsATestLabel"
id="xpack.observability.testComponent.div.thisIsATestLabel"
defaultMessage="This is a test"
/></div>
)
@ -72,7 +75,7 @@ function TestComponent() {
},
{
name: 'A JSX element with a string literal that are inside an Eui component should take the component name of the parent into account',
filename: 'x-pack/plugins/observability/public/another_component.tsx',
filename: '/x-pack/plugins/observability/public/another_component.tsx',
code: `
import React from 'react';
@ -90,7 +93,7 @@ function AnotherComponent() {
errors: [
{
line: 9,
message: `Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -104,7 +107,7 @@ function AnotherComponent() {
<EuiFlexItem>
<EuiButton>
<FormattedMessage
id="app_not_found_in_i18nrc.anotherComponent.thisIsATestButtonLabel"
id="xpack.observability.anotherComponent.thisIsATestButtonLabel"
defaultMessage="This is a test"
/></EuiButton>
</EuiFlexItem>
@ -115,7 +118,7 @@ function AnotherComponent() {
},
{
name: 'When no import of the translation module is present, the import line should be added',
filename: 'x-pack/plugins/observability/public/yet_another_component.tsx',
filename: '/x-pack/plugins/observability/public/yet_another_component.tsx',
code: `
import React from 'react';
@ -129,7 +132,7 @@ function YetAnotherComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -141,7 +144,7 @@ function YetAnotherComponent() {
<div>
<EuiSelect>
<FormattedMessage
id="app_not_found_in_i18nrc.yetAnotherComponent.selectMeSelectLabel"
id="xpack.observability.yetAnotherComponent.selectMeSelectLabel"
defaultMessage="Select me"
/></EuiSelect>
</div>
@ -150,7 +153,7 @@ function YetAnotherComponent() {
},
{
name: 'Import lines without the necessary translation module should be updated to include i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
import { SomeOtherModule } from '@kbn/i18n-react';
@ -163,7 +166,7 @@ function TestComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -172,13 +175,13 @@ import { SomeOtherModule, FormattedMessage } from '@kbn/i18n-react';
function TestComponent() {
return (
<SomeChildComponent label={<FormattedMessage id="app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
<SomeChildComponent label={<FormattedMessage id="xpack.observability.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
)
}`,
},
{
name: 'JSX elements that have a label or aria-label prop with a string value should be translated with i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
@ -191,7 +194,7 @@ function TestComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -200,13 +203,13 @@ import { FormattedMessage } from '@kbn/i18n-react';
function TestComponent() {
return (
<SomeChildComponent label={<FormattedMessage id="app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
<SomeChildComponent label={<FormattedMessage id="xpack.observability.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
)
}`,
},
{
name: 'JSX elements that have a label or aria-label prop with a JSXExpression value that is a string should be translated with i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
@ -219,7 +222,7 @@ function TestComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -228,7 +231,7 @@ function TestComponent() {
function TestComponent() {
return (
<SomeChildComponent label={<FormattedMessage id="app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
<SomeChildComponent label={<FormattedMessage id="xpack.observability.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
)
}`,
},
@ -237,7 +240,7 @@ function TestComponent() {
const valid: RuleTester.ValidTestCase[] = [
{
name: 'A JSXText element inside a EuiCode component should not be translated',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
@ -249,7 +252,7 @@ function TestComponent() {
},
{
name: 'A JSXText element that contains anything other than alpha characters should not be translated',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
@ -261,7 +264,7 @@ function TestComponent() {
},
{
name: 'A JSXText element that is wrapped in three backticks (markdown) should not be translated',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';

View file

@ -14,6 +14,8 @@ import { getFunctionName } from '../helpers/get_function_name';
import { getI18nImportFixer } from '../helpers/get_i18n_import_fixer';
import { cleanString, isTruthy } from '../helpers/utils';
export const RULE_WARNING_MESSAGE =
'Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.';
export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = {
meta: {
type: 'suggestion',
@ -44,17 +46,16 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = {
const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel'
// Check if i18n has already been imported into the file
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } =
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } =
getI18nImportFixer({
sourceCode,
mode: 'FormattedMessage',
translationFunction: 'FormattedMessage',
});
// Show warning to developer and offer autofix suggestion
report({
node: node as any,
message:
'Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.',
message: RULE_WARNING_MESSAGE,
fix(fixer) {
return [
fixer.replaceText(
@ -65,7 +66,7 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = {
/>`
),
!hasI18nImportLine && rangeToAddI18nImportLine
? mode === 'replace'
? replaceMode === 'replace'
? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine)
: fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`)
: null,
@ -106,17 +107,16 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = {
const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel'
// Check if i18n has already been imported into the file.
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } =
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } =
getI18nImportFixer({
sourceCode,
mode: 'FormattedMessage',
translationFunction: 'FormattedMessage',
});
// Show warning to developer and offer autofix suggestion
report({
node: node as any,
message:
'Strings should be translated with <FormattedMessage />. Use the autofix suggestion or add your own.',
message: RULE_WARNING_MESSAGE,
fix(fixer) {
return [
fixer.replaceTextRange(
@ -124,7 +124,7 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = {
`{<FormattedMessage id="${translationIdSuggestion}" defaultMessage="${val}" />}`
),
!hasI18nImportLine && rangeToAddI18nImportLine
? mode === 'replace'
? replaceMode === 'replace'
? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine)
: fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`)
: null,

View file

@ -7,7 +7,10 @@
*/
import { RuleTester } from 'eslint';
import { StringsShouldBeTranslatedWithI18n } from './strings_should_be_translated_with_i18n';
import {
StringsShouldBeTranslatedWithI18n,
RULE_WARNING_MESSAGE,
} from './strings_should_be_translated_with_i18n';
const tsTester = [
'@typescript-eslint/parser',
@ -41,7 +44,7 @@ const babelTester = [
const invalid: RuleTester.InvalidTestCase[] = [
{
name: 'A JSX element with a string literal should be translated with i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
@ -53,7 +56,7 @@ function TestComponent() {
errors: [
{
line: 6,
message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -62,13 +65,13 @@ import { i18n } from '@kbn/i18n';
function TestComponent() {
return (
<div>{i18n.translate('app_not_found_in_i18nrc.testComponent.div.thisIsATestLabel', { defaultMessage: 'This is a test' })}</div>
<div>{i18n.translate('xpack.observability.testComponent.div.thisIsATestLabel', { defaultMessage: 'This is a test' })}</div>
)
}`,
},
{
name: 'A JSX element with a string literal that are inside an Eui component should take the component name of the parent into account',
filename: 'x-pack/plugins/observability/public/another_component.tsx',
filename: '/x-pack/plugins/observability/public/another_component.tsx',
code: `
import React from 'react';
@ -86,7 +89,7 @@ function AnotherComponent() {
errors: [
{
line: 9,
message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -98,7 +101,7 @@ function AnotherComponent() {
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton>{i18n.translate('app_not_found_in_i18nrc.anotherComponent.thisIsATestButtonLabel', { defaultMessage: 'This is a test' })}</EuiButton>
<EuiButton>{i18n.translate('xpack.observability.anotherComponent.thisIsATestButtonLabel', { defaultMessage: 'This is a test' })}</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
@ -107,7 +110,7 @@ function AnotherComponent() {
},
{
name: 'When no import of the translation module is present, the import line should be added',
filename: 'x-pack/plugins/observability/public/yet_another_component.tsx',
filename: '/x-pack/plugins/observability/public/yet_another_component.tsx',
code: `
import React from 'react';
@ -121,7 +124,7 @@ function YetAnotherComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -131,14 +134,14 @@ import { i18n } from '@kbn/i18n';
function YetAnotherComponent() {
return (
<div>
<EuiSelect>{i18n.translate('app_not_found_in_i18nrc.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: 'Select me' })}</EuiSelect>
<EuiSelect>{i18n.translate('xpack.observability.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: 'Select me' })}</EuiSelect>
</div>
)
}`,
},
{
name: 'Import lines without the necessary translation module should be updated to include i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
import { SomeOtherModule } from '@kbn/i18n';
@ -151,7 +154,7 @@ function TestComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -160,13 +163,13 @@ import { SomeOtherModule, i18n } from '@kbn/i18n';
function TestComponent() {
return (
<SomeChildComponent label={i18n.translate('app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
<SomeChildComponent label={i18n.translate('xpack.observability.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
)
}`,
},
{
name: 'JSX elements that have a label or aria-label prop with a string value should be translated with i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
import { i18n } from '@kbn/i18n';
@ -179,7 +182,7 @@ function TestComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -188,13 +191,13 @@ import { i18n } from '@kbn/i18n';
function TestComponent() {
return (
<SomeChildComponent label={i18n.translate('app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
<SomeChildComponent label={i18n.translate('xpack.observability.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
)
}`,
},
{
name: 'JSX elements that have a label or aria-label prop with a JSXExpression value that is a string should be translated with i18n',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
import { i18n } from '@kbn/i18n';
@ -207,7 +210,7 @@ function TestComponent() {
errors: [
{
line: 7,
message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -216,7 +219,7 @@ import { i18n } from '@kbn/i18n';
function TestComponent() {
return (
<SomeChildComponent label={i18n.translate('app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
<SomeChildComponent label={i18n.translate('xpack.observability.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
)
}`,
},
@ -225,7 +228,7 @@ function TestComponent() {
const valid: RuleTester.ValidTestCase[] = [
{
name: 'A JSXText element inside a EuiCode component should not be translated',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
@ -237,7 +240,7 @@ function TestComponent() {
},
{
name: 'A JSXText element that contains anything other than alpha characters should not be translated',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';
@ -249,7 +252,7 @@ function TestComponent() {
},
{
name: 'A JSXText element that is wrapped in three backticks (markdown) should not be translated',
filename: 'x-pack/plugins/observability/public/test_component.tsx',
filename: '/x-pack/plugins/observability/public/test_component.tsx',
code: `
import React from 'react';

View file

@ -14,6 +14,9 @@ import { getFunctionName } from '../helpers/get_function_name';
import { getI18nImportFixer } from '../helpers/get_i18n_import_fixer';
import { cleanString, isTruthy } from '../helpers/utils';
export const RULE_WARNING_MESSAGE =
'Strings should be translated with i18n. Use the autofix suggestion or add your own.';
export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = {
meta: {
type: 'suggestion',
@ -44,17 +47,16 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = {
const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel'
// Check if i18n has already been imported into the file
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } =
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } =
getI18nImportFixer({
sourceCode,
mode: 'i18n.translate',
translationFunction: 'i18n.translate',
});
// Show warning to developer and offer autofix suggestion
report({
node: node as any,
message:
'Strings should be translated with i18n. Use the autofix suggestion or add your own.',
message: RULE_WARNING_MESSAGE,
fix(fixer) {
return [
fixer.replaceText(
@ -62,7 +64,7 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = {
`${whiteSpaces}{i18n.translate('${translationIdSuggestion}', { defaultMessage: '${value}' })}`
),
!hasI18nImportLine && rangeToAddI18nImportLine
? mode === 'replace'
? replaceMode === 'replace'
? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine)
: fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`)
: null,
@ -103,17 +105,16 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = {
const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel'
// Check if i18n has already been imported into the file.
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } =
const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } =
getI18nImportFixer({
sourceCode,
mode: 'i18n.translate',
translationFunction: 'i18n.translate',
});
// Show warning to developer and offer autofix suggestion
report({
node: node as any,
message:
'Strings should be translated with i18n. Use the autofix suggestion or add your own.',
message: RULE_WARNING_MESSAGE,
fix(fixer) {
return [
fixer.replaceTextRange(
@ -121,7 +122,7 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = {
`{i18n.translate('${translationIdSuggestion}', { defaultMessage: '${val}' })}`
),
!hasI18nImportLine && rangeToAddI18nImportLine
? mode === 'replace'
? replaceMode === 'replace'
? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine)
: fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`)
: null,

View file

@ -10,7 +10,7 @@ export {
getDataGridSchemaFromESFieldType,
getDataGridSchemaFromKibanaFieldType,
getFeatureImportance,
getFieldsFromKibanaIndexPattern,
getFieldsFromKibanaDataView,
getNestedOrEscapedVal,
getProcessedFields,
getTopClasses,

View file

@ -83,7 +83,7 @@ export const euiDataGridToolbarSettings = {
* @param {DataView} dataView - The Kibana data view.
* @returns {string[]} - The array of field names from the data view.
*/
export const getFieldsFromKibanaIndexPattern = (dataView: DataView): string[] => {
export const getFieldsFromKibanaDataView = (dataView: DataView): string[] => {
const allFields = dataView.fields.map((f) => f.name);
const dataViewFields: string[] = allFields.filter((f) => {
if (dataView.metaFields.includes(f)) {

View file

@ -158,9 +158,9 @@ export interface UseIndexDataReturnType
*/
renderCellValue: RenderCellValue;
/**
* Optional index pattern fields.
* Optional data view fields.
*/
indexPatternFields?: string[];
dataViewFields?: string[];
/**
* Optional time range.
*/

View file

@ -18,3 +18,4 @@ export const GET_ASSETS_DIFF = base('/assets/diff');
export const GET_HOSTS = base('/assets/hosts');
export const GET_SERVICES = base('/assets/services');
export const GET_CONTAINERS = base('/assets/containers');
export const GET_PODS = base('/assets/pods');

View file

@ -172,6 +172,7 @@ export const assetFiltersSingleKindRT = rt.exact(
id: rt.string,
['cloud.provider']: rt.string,
['cloud.region']: rt.string,
['orchestrator.cluster.name']: rt.string,
})
);
@ -258,3 +259,21 @@ export const getServiceAssetsResponseRT = rt.type({
services: rt.array(assetRT),
});
export type GetServiceAssetsResponse = rt.TypeOf<typeof getServiceAssetsResponseRT>;
/**
* Pods
*/
export const getPodAssetsQueryOptionsRT = rt.intersection([
rt.strict({ from: assetDateRT }),
rt.partial({
to: assetDateRT,
size: sizeRT,
stringFilters: rt.string,
filters: assetFiltersSingleKindRT,
}),
]);
export type GetPodAssetsQueryOptions = rt.TypeOf<typeof getPodAssetsQueryOptionsRT>;
export const getPodAssetsResponseRT = rt.type({
pods: rt.array(assetRT),
});
export type GetPodAssetsResponse = rt.TypeOf<typeof getPodAssetsResponseRT>;

View file

@ -19,6 +19,7 @@ export interface SharedAssetsOptionsPublic<F = AssetFilters> {
export type GetHostsOptionsPublic = SharedAssetsOptionsPublic<SingleKindAssetFilters>;
export type GetContainersOptionsPublic = SharedAssetsOptionsPublic<SingleKindAssetFilters>;
export type GetPodsOptionsPublic = SharedAssetsOptionsPublic<SingleKindAssetFilters>;
export interface GetServicesOptionsPublic
extends SharedAssetsOptionsPublic<SingleKindAssetFilters> {

View file

@ -10,13 +10,15 @@ import {
GetContainersOptionsPublic,
GetHostsOptionsPublic,
GetServicesOptionsPublic,
GetPodsOptionsPublic,
} from '../../common/types_client';
import {
GetContainerAssetsResponse,
GetHostAssetsResponse,
GetServiceAssetsResponse,
GetPodAssetsResponse,
} from '../../common/types_api';
import { GET_CONTAINERS, GET_HOSTS, GET_SERVICES } from '../../common/constants_routes';
import { GET_CONTAINERS, GET_HOSTS, GET_SERVICES, GET_PODS } from '../../common/constants_routes';
import { IPublicAssetsClient } from '../types';
export class PublicAssetsClient implements IPublicAssetsClient {
@ -57,4 +59,16 @@ export class PublicAssetsClient implements IPublicAssetsClient {
return results;
}
async getPods(options: GetPodsOptionsPublic) {
const { filters, ...otherOptions } = options;
const results = await this.http.get<GetPodAssetsResponse>(GET_PODS, {
query: {
stringFilters: JSON.stringify(filters),
...otherOptions,
},
});
return results;
}
}

View file

@ -32,7 +32,7 @@ function createBaseOptions({
};
}
describe('getHosts', () => {
describe('getContainers', () => {
let getApmIndicesMock = createGetApmIndicesMock();
let metricsDataClientMock = MetricsDataClientMock.create();
let baseOptions = createBaseOptions({ getApmIndicesMock, metricsDataClientMock });

View file

@ -0,0 +1,341 @@
/*
* 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 { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { GetApmIndicesMethod } from '../../asset_client_types';
import { getPods } from './get_pods';
import {
createGetApmIndicesMock,
expectToThrowValidationErrorWithStatusCode,
} from '../../../test_utils';
import { MetricsDataClient, MetricsDataClientMock } from '@kbn/metrics-data-access-plugin/server';
import { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
function createBaseOptions({
getApmIndicesMock,
metricsDataClientMock,
}: {
getApmIndicesMock: GetApmIndicesMethod;
metricsDataClientMock: MetricsDataClient;
}) {
return {
sourceIndices: {
logs: 'my-logs*',
},
getApmIndices: getApmIndicesMock,
metricsClient: metricsDataClientMock,
};
}
describe('getPods', () => {
let getApmIndicesMock = createGetApmIndicesMock();
let metricsDataClientMock = MetricsDataClientMock.create();
let baseOptions = createBaseOptions({ getApmIndicesMock, metricsDataClientMock });
let esClientMock = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
let soClientMock = savedObjectsClientMock.create();
function resetMocks() {
getApmIndicesMock = createGetApmIndicesMock();
metricsDataClientMock = MetricsDataClientMock.create();
baseOptions = createBaseOptions({ getApmIndicesMock, metricsDataClientMock });
esClientMock = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
soClientMock = savedObjectsClientMock.create();
}
beforeEach(() => {
resetMocks();
// ES returns no results, just enough structure to not blow up
esClientMock.search.mockResolvedValueOnce({
took: 1,
timed_out: false,
_shards: {
failed: 0,
successful: 1,
total: 1,
},
hits: {
hits: [],
},
});
});
it('should query Elasticsearch correctly', async () => {
await getPods({
...baseOptions,
from: 'now-5d',
to: 'now-3d',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
});
expect(metricsDataClientMock.getMetricIndices).toHaveBeenCalledTimes(1);
expect(metricsDataClientMock.getMetricIndices).toHaveBeenCalledWith({
savedObjectsClient: soClientMock,
});
const dsl = esClientMock.search.mock.lastCall?.[0] as SearchRequest | undefined;
const { bool } = dsl?.query || {};
expect(bool).toBeDefined();
expect(bool?.filter).toEqual([
{
range: {
'@timestamp': {
gte: 'now-5d',
lte: 'now-3d',
},
},
},
]);
expect(bool?.must).toEqual([
{
exists: {
field: 'kubernetes.pod.uid',
},
},
{
exists: {
field: 'kubernetes.node.name',
},
},
]);
});
it('should correctly include an EAN filter as a pod ID term query', async () => {
const mockPodId = '123abc';
await getPods({
...baseOptions,
from: 'now-1h',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
filters: {
ean: `pod:${mockPodId}`,
},
});
const dsl = esClientMock.search.mock.lastCall?.[0] as SearchRequest | undefined;
const { bool } = dsl?.query || {};
expect(bool).toBeDefined();
expect(bool?.must).toEqual(
expect.arrayContaining([
{
exists: {
field: 'kubernetes.pod.uid',
},
},
{
exists: {
field: 'kubernetes.node.name',
},
},
{
term: {
'kubernetes.pod.uid': mockPodId,
},
},
])
);
});
it('should not query ES and return empty if filtering on non-pod EAN', async () => {
const mockId = 'some-id-123';
const result = await getPods({
...baseOptions,
from: 'now-1h',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
filters: {
ean: `container:${mockId}`,
},
});
expect(esClientMock.search).toHaveBeenCalledTimes(0);
expect(result).toEqual({ pods: [] });
});
it('should include a wildcard ID filter when an ID filter is provided with asterisks included', async () => {
const mockIdPattern = '*partial-id*';
await getPods({
...baseOptions,
from: 'now-1h',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
filters: {
id: mockIdPattern,
},
});
const dsl = esClientMock.search.mock.lastCall?.[0] as SearchRequest | undefined;
const { bool } = dsl?.query || {};
expect(bool).toBeDefined();
expect(bool?.must).toEqual(
expect.arrayContaining([
{
exists: {
field: 'kubernetes.pod.uid',
},
},
{
exists: {
field: 'kubernetes.node.name',
},
},
{
wildcard: {
'kubernetes.pod.uid': mockIdPattern,
},
},
])
);
});
it('should include a term ID filter when an ID filter is provided without asterisks included', async () => {
const mockId = 'full-id';
await getPods({
...baseOptions,
from: 'now-1h',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
filters: {
id: mockId,
},
});
const dsl = esClientMock.search.mock.lastCall?.[0] as SearchRequest | undefined;
const { bool } = dsl?.query || {};
expect(bool).toBeDefined();
expect(bool?.must).toEqual(
expect.arrayContaining([
{
exists: {
field: 'kubernetes.pod.uid',
},
},
{
exists: {
field: 'kubernetes.node.name',
},
},
{
term: {
'kubernetes.pod.uid': mockId,
},
},
])
);
});
it('should include a term filter for cloud filters', async () => {
const mockCloudProvider = 'gcp';
const mockCloudRegion = 'us-central-1';
await getPods({
...baseOptions,
from: 'now-1h',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
filters: {
'cloud.provider': mockCloudProvider,
'cloud.region': mockCloudRegion,
},
});
const dsl = esClientMock.search.mock.lastCall?.[0] as SearchRequest | undefined;
const { bool } = dsl?.query || {};
expect(bool).toBeDefined();
expect(bool?.must).toEqual(
expect.arrayContaining([
{
exists: {
field: 'kubernetes.pod.uid',
},
},
{
exists: {
field: 'kubernetes.node.name',
},
},
{
term: {
'cloud.provider': mockCloudProvider,
},
},
{
term: {
'cloud.region': mockCloudRegion,
},
},
])
);
});
it('should reject with 400 for invalid "from" date', () => {
return expectToThrowValidationErrorWithStatusCode(
() =>
getPods({
...baseOptions,
from: 'now-1zz',
to: 'now-3d',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
}),
{ statusCode: 400 }
);
});
it('should reject with 400 for invalid "to" date', () => {
return expectToThrowValidationErrorWithStatusCode(
() =>
getPods({
...baseOptions,
from: 'now-5d',
to: 'now-3fe',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
}),
{ statusCode: 400 }
);
});
it('should reject with 400 when "from" is a date that is after "to"', () => {
return expectToThrowValidationErrorWithStatusCode(
() =>
getPods({
...baseOptions,
from: 'now',
to: 'now-5d',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
}),
{ statusCode: 400 }
);
});
it('should reject with 400 when "from" is in the future', () => {
return expectToThrowValidationErrorWithStatusCode(
() =>
getPods({
...baseOptions,
from: 'now+1d',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
}),
{ statusCode: 400 }
);
});
});

View file

@ -0,0 +1,96 @@
/*
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { Asset } from '../../../../common/types_api';
import { GetPodsOptionsPublic } from '../../../../common/types_client';
import {
AssetClientDependencies,
AssetClientOptionsWithInjectedValues,
} from '../../asset_client_types';
import { parseEan } from '../../parse_ean';
import { collectPods } from '../../collectors/pods';
import { validateStringDateRange } from '../../validators/validate_date_range';
export type GetPodsOptions = GetPodsOptionsPublic & AssetClientDependencies;
export type GetPodsOptionsInjected = AssetClientOptionsWithInjectedValues<GetPodsOptions>;
export async function getPods(options: GetPodsOptionsInjected): Promise<{ pods: Asset[] }> {
validateStringDateRange(options.from, options.to);
const metricsIndices = await options.metricsClient.getMetricIndices({
savedObjectsClient: options.savedObjectsClient,
});
const filters: QueryDslQueryContainer[] = [];
if (options.filters?.ean) {
const ean = Array.isArray(options.filters.ean) ? options.filters.ean[0] : options.filters.ean;
const { kind, id } = parseEan(ean);
// if EAN filter isn't targeting a pod asset, we don't need to do this query
if (kind !== 'pod') {
return {
pods: [],
};
}
filters.push({
term: {
'kubernetes.pod.uid': id,
},
});
}
if (options.filters?.id) {
const fn = options.filters.id.includes('*') ? 'wildcard' : 'term';
filters.push({
[fn]: {
'kubernetes.pod.uid': options.filters.id,
},
});
}
if (options.filters?.['orchestrator.cluster.name']) {
filters.push({
term: {
'orchestrator.cluster.name': options.filters['orchestrator.cluster.name'],
},
});
}
if (options.filters?.['cloud.provider']) {
filters.push({
term: {
'cloud.provider': options.filters['cloud.provider'],
},
});
}
if (options.filters?.['cloud.region']) {
filters.push({
term: {
'cloud.region': options.filters['cloud.region'],
},
});
}
const { assets } = await collectPods({
client: options.elasticsearchClient,
from: options.from,
to: options.to || 'now',
filters,
sourceIndices: {
metrics: metricsIndices,
logs: options.sourceIndices.logs,
},
});
return {
pods: assets,
};
}

View file

@ -9,6 +9,7 @@ import { Asset } from '../../common/types_api';
import { getContainers, GetContainersOptions } from './accessors/containers/get_containers';
import { getHosts, GetHostsOptions } from './accessors/hosts/get_hosts';
import { getServices, GetServicesOptions } from './accessors/services/get_services';
import { getPods, GetPodsOptions } from './accessors/pods/get_pods';
import { AssetClientBaseOptions, AssetClientOptionsWithInjectedValues } from './asset_client_types';
export class AssetClient {
@ -35,4 +36,9 @@ export class AssetClient {
const withInjected = this.injectOptions(options);
return await getContainers(withInjected);
}
async getPods(options: GetPodsOptions): Promise<{ pods: Asset[] }> {
const withInjected = this.injectOptions(options);
return await getPods(withInjected);
}
}

View file

@ -9,11 +9,24 @@ import { estypes } from '@elastic/elasticsearch';
import { Asset } from '../../../common/types_api';
import { CollectorOptions, QUERY_MAX_SIZE } from '.';
export async function collectPods({ client, from, to, sourceIndices, afterKey }: CollectorOptions) {
export async function collectPods({
client,
from,
to,
sourceIndices,
filters = [],
afterKey,
}: CollectorOptions) {
if (!sourceIndices?.metrics || !sourceIndices?.logs) {
throw new Error('missing required metrics/logs indices');
}
const musts = [
...filters,
{ exists: { field: 'kubernetes.pod.uid' } },
{ exists: { field: 'kubernetes.node.name' } },
];
const { metrics, logs } = sourceIndices;
const dsl: estypes.SearchRequest = {
index: [metrics, logs],
@ -42,10 +55,7 @@ export async function collectPods({ client, from, to, sourceIndices, afterKey }:
},
},
],
must: [
{ exists: { field: 'kubernetes.pod.uid' } },
{ exists: { field: 'kubernetes.node.name' } },
],
must: musts,
},
},
};

View file

@ -0,0 +1,70 @@
/*
* 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 { createRouteValidationFunction } from '@kbn/io-ts-utils';
import { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import { GetPodAssetsQueryOptions, getPodAssetsQueryOptionsRT } from '../../../common/types_api';
import { debug } from '../../../common/debug_log';
import { SetupRouteOptions } from '../types';
import * as routePaths from '../../../common/constants_routes';
import { getClientsFromContext, validateStringAssetFilters } from '../utils';
import { AssetsValidationError } from '../../lib/validators/validation_error';
export function podsRoutes<T extends RequestHandlerContext>({
router,
assetClient,
}: SetupRouteOptions<T>) {
const validate = createRouteValidationFunction(getPodAssetsQueryOptionsRT);
router.get<unknown, GetPodAssetsQueryOptions, unknown>(
{
path: routePaths.GET_PODS,
validate: {
query: (q, res) => {
const [invalidResponse, validatedFilters] = validateStringAssetFilters(q, res);
if (invalidResponse) {
return invalidResponse;
}
if (validatedFilters) {
q.filters = validatedFilters;
}
return validate(q, res);
},
},
},
async (context, req, res) => {
const { from = 'now-24h', to = 'now', filters } = req.query || {};
const { elasticsearchClient, savedObjectsClient } = await getClientsFromContext(context);
try {
const response = await assetClient.getPods({
from,
to,
filters,
elasticsearchClient,
savedObjectsClient,
});
return res.ok({ body: response });
} catch (error: unknown) {
debug('Error while looking up POD asset records', error);
if (error instanceof AssetsValidationError) {
return res.customError({
statusCode: error.statusCode,
body: {
message: `Error while looking up pod asset records - ${error.message}`,
},
});
}
return res.customError({
statusCode: 500,
body: { message: 'Error while looking up pod asset records - ' + `${error}` },
});
}
}
);
}

View file

@ -12,6 +12,7 @@ import { sampleAssetsRoutes } from './sample_assets';
import { hostsRoutes } from './assets/hosts';
import { servicesRoutes } from './assets/services';
import { containersRoutes } from './assets/containers';
import { podsRoutes } from './assets/pods';
export function setupRoutes<T extends RequestHandlerContext>({
router,
@ -22,4 +23,5 @@ export function setupRoutes<T extends RequestHandlerContext>({
hostsRoutes<T>({ router, assetClient });
servicesRoutes<T>({ router, assetClient });
containersRoutes<T>({ router, assetClient });
podsRoutes<T>({ router, assetClient });
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { formatFieldValue } from '@kbn/discover-utils';
import he from 'he';
import * as constants from '../../../common/constants';
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
import { FlyoutDoc, FlyoutProps, LogDocument } from './types';
@ -34,8 +33,7 @@ export function useDocDetail(
// Flyout Headers
const level = formatField(constants.LOG_LEVEL_FIELD)?.toLowerCase();
const timestamp = formatField(constants.TIMESTAMP_FIELD);
const formattedMessage = formatField(constants.MESSAGE_FIELD);
const message = formattedMessage ? he.decode(formattedMessage) : undefined;
const message = doc.flattened[constants.MESSAGE_FIELD];
// Service Highlights
const serviceName = formatField(constants.SERVICE_NAME_FIELD);

View file

@ -101,7 +101,7 @@ export interface ScatterplotMatrixProps {
legendType?: LegendType;
searchQuery?: estypes.QueryDslQueryContainer;
runtimeMappings?: RuntimeMappings;
indexPattern?: DataView;
dataView?: DataView;
query?: Query;
}
@ -113,7 +113,7 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
legendType,
searchQuery,
runtimeMappings,
indexPattern,
dataView,
query,
}) => {
const { esSearch } = useMlApiContext();
@ -210,9 +210,7 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
vegaSpec.data = {
url: {
'%context%': true,
...(indexPattern?.timeFieldName
? { ['%timefield%']: `${indexPattern?.timeFieldName}` }
: {}),
...(dataView?.timeFieldName ? { ['%timefield%']: `${dataView?.timeFieldName}` } : {}),
index,
body: {
fields: fieldsToFetch,
@ -300,7 +298,7 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
}
const combinedRuntimeMappings =
indexPattern && getCombinedRuntimeMappings(indexPattern, runtimeMappings);
dataView && getCombinedRuntimeMappings(dataView, runtimeMappings);
const body = {
fields: queryFields,

View file

@ -37,12 +37,10 @@ export const useResultsViewConfig = (jobId: string) => {
} = useMlKibana();
const trainedModelsApiService = useTrainedModelsApiService();
const [indexPattern, setIndexPattern] = useState<DataView | undefined>(undefined);
const [indexPatternErrorMessage, setIndexPatternErrorMessage] = useState<undefined | string>(
undefined
);
const [dataView, setDataView] = useState<DataView | undefined>(undefined);
const [dataViewErrorMessage, setDataViewErrorMessage] = useState<undefined | string>(undefined);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [needsDestIndexPattern, setNeedsDestIndexPattern] = useState<boolean>(false);
const [needsDestDataView, setNeedsDestDataView] = useState<boolean>(false);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false);
const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined);
const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState<undefined | string>(
@ -100,39 +98,39 @@ export const useResultsViewConfig = (jobId: string) => {
try {
const destIndex = getDestinationIndex(jobConfigUpdate);
const destDataViewId = (await getDataViewIdFromName(destIndex)) ?? destIndex;
let dataView: DataView | undefined;
let fetchedDataView: DataView | undefined;
try {
dataView = await dataViews.get(destDataViewId);
fetchedDataView = await dataViews.get(destDataViewId);
// Force refreshing the fields list here because a user directly coming
// from the job creation wizard might land on the page without the
// data view being fully initialized because it was created
// before the analytics job populated the destination index.
await dataViews.refreshFields(dataView);
await dataViews.refreshFields(fetchedDataView);
} catch (e) {
dataView = undefined;
fetchedDataView = undefined;
}
if (dataView === undefined) {
setNeedsDestIndexPattern(true);
if (fetchedDataView === undefined) {
setNeedsDestDataView(true);
const sourceIndex = jobConfigUpdate.source.index[0];
const sourceDataViewId = (await getDataViewIdFromName(sourceIndex)) ?? sourceIndex;
try {
dataView = await dataViews.get(sourceDataViewId);
fetchedDataView = await dataViews.get(sourceDataViewId);
} catch (e) {
dataView = undefined;
fetchedDataView = undefined;
}
}
if (dataView !== undefined) {
await newJobCapsServiceAnalytics.initializeFromDataVIew(dataView);
if (fetchedDataView !== undefined) {
await newJobCapsServiceAnalytics.initializeFromDataVIew(fetchedDataView);
setJobConfig(analyticsConfigs.data_frame_analytics[0]);
setIndexPattern(dataView);
setDataView(fetchedDataView);
setIsInitialized(true);
setIsLoadingJobConfig(false);
} else {
setIndexPatternErrorMessage(
setDataViewErrorMessage(
i18n.translate('xpack.ml.dataframe.analytics.results.dataViewMissingErrorMessage', {
defaultMessage:
'To view this page, a Kibana data view is necessary for either the destination or source index of this analytics job.',
@ -153,15 +151,15 @@ export const useResultsViewConfig = (jobId: string) => {
}, []);
return {
indexPattern,
indexPatternErrorMessage,
dataView,
dataViewErrorMessage,
isInitialized,
isLoadingJobConfig,
jobCapsServiceErrorMessage,
jobConfig,
jobConfigErrorMessage,
jobStatus,
needsDestIndexPattern,
needsDestDataView,
totalFeatureImportance,
};
};

View file

@ -356,9 +356,9 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const indexPatternFieldsTableItems = useMemo(() => {
if (indexData?.indexPatternFields !== undefined) {
return indexData.indexPatternFields.map((field) => ({
const dataViewFieldsTableItems = useMemo(() => {
if (indexData?.dataViewFields !== undefined) {
return indexData.dataViewFields.map((field) => ({
name: field,
is_included: false,
is_required: false,
@ -366,7 +366,7 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
}
return [];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [`${indexData?.indexPatternFields}`]);
}, [`${indexData?.dataViewFields}`]);
useEffect(() => {
if (typeof savedSearchQueryStr === 'string') {
@ -377,11 +377,11 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
useEffect(() => {
if (isJobTypeWithDepVar) {
const indexPatternRuntimeFields = getCombinedRuntimeMappings(selectedDataView);
const dataViewRuntimeFields = getCombinedRuntimeMappings(selectedDataView);
let runtimeOptions;
if (indexPatternRuntimeFields) {
runtimeOptions = getRuntimeDepVarOptions(jobType, indexPatternRuntimeFields);
if (dataViewRuntimeFields) {
runtimeOptions = getRuntimeDepVarOptions(jobType, dataViewRuntimeFields);
}
loadDepVarOptions(form, runtimeOptions);
@ -527,7 +527,7 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
legendType: getScatterplotMatrixLegendType(jobType),
searchQuery: jobConfigQuery,
runtimeMappings,
indexPattern: selectedDataView,
dataView: selectedDataView,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
@ -571,7 +571,7 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
const tableItems =
includesTableItems.length > 0 && !noDocsContainMappedFields
? includesTableItems
: indexPatternFieldsTableItems;
: dataViewFieldsTableItems;
return (
<FieldStatsFlyoutProvider
@ -592,7 +592,7 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
fullWidth
>
<ExplorationQueryBar
indexPattern={selectedDataView}
dataView={selectedDataView}
setSearchQuery={setJobConfigQuery}
query={query}
/>

View file

@ -42,13 +42,8 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
const { createAnalyticsJob, setFormState, startAnalyticsJob } = actions;
const { isAdvancedEditorValidJson, isJobCreated, isJobStarted, isValid, requestMessages } = state;
const {
createIndexPattern,
destinationIndex,
destinationIndexPatternTitleExists,
jobId,
jobType,
} = state.form;
const { createDataView, destinationIndex, destinationDataViewTitleExists, jobId, jobType } =
state.form;
const [startChecked, setStartChecked] = useState<boolean>(true);
const [creationTriggered, setCreationTriggered] = useState<boolean>(false);
@ -56,7 +51,7 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
useEffect(() => {
if (canCreateDataView === false) {
setFormState({ createIndexPattern: false });
setFormState({ createDataView: false });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [capabilities]);
@ -106,7 +101,7 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
onChange={(e) => {
setStartChecked(e.target.checked);
if (e.target.checked === false) {
setFormState({ createIndexPattern: false });
setFormState({ createDataView: false });
}
}}
/>
@ -117,8 +112,8 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
<EuiFormRow
fullWidth
isInvalid={
(createIndexPattern && destinationIndexPatternTitleExists) ||
createIndexPattern === false ||
(createDataView && destinationDataViewTitleExists) ||
createDataView === false ||
canCreateDataView === false
}
error={[
@ -134,7 +129,7 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
</EuiText>,
]
: []),
...(createIndexPattern && destinationIndexPatternTitleExists
...(createDataView && destinationDataViewTitleExists
? [
i18n.translate(
'xpack.ml.dataframe.analytics.create.dataViewExistsError',
@ -146,7 +141,7 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
),
]
: []),
...(!createIndexPattern && !destinationIndexPatternTitleExists
...(!createDataView && !destinationDataViewTitleExists
? [
<EuiText size="xs" color="warning">
{i18n.translate(
@ -171,8 +166,8 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
defaultMessage: 'Create data view',
}
)}
checked={createIndexPattern === true}
onChange={() => setFormState({ createIndexPattern: !createIndexPattern })}
checked={createDataView === true}
onChange={() => setFormState({ createDataView: !createDataView })}
data-test-subj="mlAnalyticsCreateJobWizardCreateDataViewCheckbox"
/>
</EuiFormRow>
@ -186,7 +181,7 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
disabled={
!isValid ||
!isAdvancedEditorValidJson ||
(destinationIndexPatternTitleExists === true && createIndexPattern === true)
(destinationDataViewTitleExists === true && createDataView === true)
}
onClick={handleCreation}
fill

View file

@ -25,7 +25,7 @@ import {
getFieldType,
getDataGridSchemaFromKibanaFieldType,
getDataGridSchemaFromESFieldType,
getFieldsFromKibanaIndexPattern,
getFieldsFromKibanaDataView,
showDataGridColumnChartErrorMessageToast,
useDataGrid,
useRenderCellValue,
@ -58,8 +58,8 @@ function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) {
});
}
function getIndexPatternColumns(indexPattern: DataView, fieldsFilter: string[]) {
const { fields } = indexPattern;
function getDataViewColumns(dataView: DataView, fieldsFilter: string[]) {
const { fields } = dataView;
return fields
.filter((field) => fieldsFilter.includes(field.name))
@ -78,7 +78,7 @@ function getIndexPatternColumns(indexPattern: DataView, fieldsFilter: string[])
}
export const useIndexData = (
indexPattern: DataView,
dataView: DataView,
query: Record<string, any> | undefined,
toastNotifications: CoreSetup['notifications']['toasts'],
runtimeMappings?: RuntimeMappings
@ -87,7 +87,7 @@ export const useIndexData = (
// This is a workaround to avoid passing potentially thousands of unpopulated fields
// (for example, as part of filebeat/metricbeat/ECS based indices)
// to the data grid component which would significantly slow down the page.
const [indexPatternFields, setIndexPatternFields] = useState<string[]>();
const [dataViewFields, setDataViewFields] = useState<string[]>();
const [timeRangeMs, setTimeRangeMs] = useState<TimeRangeMs | undefined>();
useEffect(() => {
@ -96,7 +96,7 @@ export const useIndexData = (
setStatus(INDEX_STATUS.LOADING);
const esSearchRequest = {
index: indexPattern.title,
index: dataView.title,
body: {
fields: ['*'],
_source: false,
@ -116,13 +116,13 @@ export const useIndexData = (
// Get all field names for each returned doc and flatten it
// to a list of unique field names used across all docs.
const allDataViewFields = getFieldsFromKibanaIndexPattern(indexPattern);
const allDataViewFields = getFieldsFromKibanaDataView(dataView);
const populatedFields = [...new Set(docs.map(Object.keys).flat(1))]
.filter((d) => allDataViewFields.includes(d))
.sort();
setStatus(INDEX_STATUS.LOADED);
setIndexPatternFields(populatedFields);
setDataViewFields(populatedFields);
} catch (e) {
setErrorMessage(extractErrorMessage(e));
setStatus(INDEX_STATUS.ERROR);
@ -136,20 +136,20 @@ export const useIndexData = (
// To be used for data grid column selection
// and will be applied to doc and chart queries.
const combinedRuntimeMappings = useMemo(
() => getCombinedRuntimeMappings(indexPattern, runtimeMappings),
[indexPattern, runtimeMappings]
() => getCombinedRuntimeMappings(dataView, runtimeMappings),
[dataView, runtimeMappings]
);
// Available data grid columns, will be a combination of index pattern and runtime fields.
const [columns, setColumns] = useState<MLEuiDataGridColumn[]>([]);
useEffect(() => {
if (Array.isArray(indexPatternFields)) {
if (Array.isArray(dataViewFields)) {
setColumns([
...getIndexPatternColumns(indexPattern, indexPatternFields),
...getDataViewColumns(dataView, dataViewFields),
...(combinedRuntimeMappings ? getRuntimeFieldColumns(combinedRuntimeMappings) : []),
]);
}
}, [indexPattern, indexPatternFields, combinedRuntimeMappings]);
}, [dataView, dataViewFields, combinedRuntimeMappings]);
const dataGrid = useDataGrid(columns);
@ -175,19 +175,19 @@ export const useIndexData = (
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
const timeFieldName = indexPattern.getTimeField()?.name;
const timeFieldName = dataView.getTimeField()?.name;
const sort: EsSorting = sortingColumns.reduce((s, column) => {
s[column.id] = { order: column.direction };
return s;
}, {} as EsSorting);
const esSearchRequest = {
index: indexPattern.title,
index: dataView.title,
body: {
query,
from: pagination.pageIndex * pagination.pageSize,
size: pagination.pageSize,
fields: [
...(indexPatternFields ?? []),
...(dataViewFields ?? []),
...(isRuntimeMappings(combinedRuntimeMappings)
? Object.keys(combinedRuntimeMappings)
: []),
@ -246,22 +246,22 @@ export const useIndexData = (
}
}
if (indexPatternFields !== undefined && query !== undefined) {
if (dataViewFields !== undefined && query !== undefined) {
fetchIndexData();
}
// custom comparison
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
indexPattern.title,
indexPatternFields,
dataView.title,
dataViewFields,
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify([query, pagination, sortingColumns, combinedRuntimeMappings]),
]);
const dataLoader = useMemo(
() => new DataLoader(indexPattern, toastNotifications),
() => new DataLoader(dataView, toastNotifications),
// eslint-disable-next-line react-hooks/exhaustive-deps
[indexPattern]
[dataView]
);
useEffect(() => {
@ -291,16 +291,16 @@ export const useIndexData = (
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
dataGrid.chartsVisible,
indexPattern.title,
dataView.title,
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify([query, dataGrid.visibleColumns, runtimeMappings]),
]);
const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems);
const renderCellValue = useRenderCellValue(dataView, pagination, tableItems);
return {
...dataGrid,
indexPatternFields,
dataViewFields,
renderCellValue,
timeRangeMs,
};

View file

@ -15,7 +15,7 @@ interface Props {
destIndex?: string;
}
export const IndexPatternPrompt: FC<Props> = ({ destIndex, color }) => {
export const DataViewPrompt: FC<Props> = ({ destIndex, color }) => {
const {
services: {
http: { basePath },

View file

@ -59,7 +59,7 @@ import { replaceStringTokens } from '../../../../../util/string_utils';
import { parseInterval } from '../../../../../../../common/util/parse_interval';
import { ExpandableSection, ExpandableSectionProps, HEADER_ITEMS_LOADING } from '.';
import { IndexPatternPrompt } from '../index_pattern_prompt';
import { DataViewPrompt } from '../data_view_prompt';
const showingDocs = i18n.translate(
'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText',
@ -121,9 +121,9 @@ const getResultsSectionHeaderItems = (
interface ExpandableSectionResultsProps {
colorRange?: ReturnType<typeof useColorRange>;
indexData: UseIndexDataReturnType;
indexPattern?: DataView;
dataView?: DataView;
jobConfig?: DataFrameAnalyticsConfig;
needsDestIndexPattern: boolean;
needsDestDataView: boolean;
resultsField?: string;
searchQuery: estypes.QueryDslQueryContainer;
}
@ -131,9 +131,9 @@ interface ExpandableSectionResultsProps {
export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
colorRange,
indexData,
indexPattern,
dataView,
jobConfig,
needsDestIndexPattern,
needsDestDataView,
resultsField,
searchQuery,
}) => {
@ -146,7 +146,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
},
} = useMlKibana();
const dataViewId = indexPattern?.id;
const dataViewId = dataView?.id;
const discoverLocator = useMemo(
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
@ -206,7 +206,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
if (discoverLocator !== undefined) {
const url = await discoverLocator.getRedirectUrl({
indexPatternId: dataViewId,
dataViewId,
timeRange: data.query.timefilter.timefilter.getTime(),
filters: data.query.filterManager.getFilters(),
query: {
@ -239,7 +239,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
if (timeRangeInterval !== null) {
// Create a copy of the record as we are adding properties into it.
const record = cloneDeep(item);
const timestamp = record[indexPattern!.timeFieldName!];
const timestamp = record[dataView!.timeFieldName!];
const configuredUrlValue = customUrl.url_value;
if (configuredUrlValue.includes('$earliest$')) {
@ -373,9 +373,9 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
const resultsSectionContent = (
<>
{jobConfig !== undefined && needsDestIndexPattern && (
{jobConfig !== undefined && needsDestDataView && (
<div className="mlExpandableSection-contentPadding">
<IndexPatternPrompt destIndex={jobConfig.dest.index} />
<DataViewPrompt destIndex={jobConfig.dest.index} />
</div>
)}
{jobConfig !== undefined &&
@ -386,7 +386,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
</EuiText>
)}
{(columnsWithCharts.length > 0 || searchQuery !== defaultSearchQuery) &&
indexPattern !== undefined && (
dataView !== undefined && (
<>
{columnsWithCharts.length > 0 &&
(tableItems.length > 0 || status === INDEX_STATUS.LOADED) && (

View file

@ -35,7 +35,7 @@ import { LoadingPanel } from '../loading_panel';
import { FeatureImportanceSummaryPanelProps } from '../total_feature_importance_summary/feature_importance_summary';
import { useExplorationUrlState } from '../../hooks/use_exploration_url_state';
import { ExplorationQueryBarProps } from '../exploration_query_bar/exploration_query_bar';
import { IndexPatternPrompt } from '../index_pattern_prompt';
import { DataViewPrompt } from '../data_view_prompt';
function getFilters(resultsField: string) {
return {
@ -84,15 +84,15 @@ export const ExplorationPageWrapper: FC<Props> = ({
FeatureImportanceSummaryPanel,
}) => {
const {
indexPattern,
indexPatternErrorMessage,
dataView,
dataViewErrorMessage,
isInitialized,
isLoadingJobConfig,
jobCapsServiceErrorMessage,
jobConfig,
jobConfigErrorMessage,
jobStatus,
needsDestIndexPattern,
needsDestDataView,
totalFeatureImportance,
} = useResultsViewConfig(jobId);
@ -121,13 +121,13 @@ export const ExplorationPageWrapper: FC<Props> = ({
const destIndex = getDestinationIndex(jobConfig);
const scatterplotFieldOptions = useScatterplotFieldOptions(
indexPattern,
dataView,
jobConfig?.analyzed_fields?.includes,
jobConfig?.analyzed_fields?.excludes,
resultsField
);
if (indexPatternErrorMessage !== undefined) {
if (dataViewErrorMessage !== undefined) {
return (
<EuiPanel grow={false}>
<EuiCallOut
@ -138,10 +138,8 @@ export const ExplorationPageWrapper: FC<Props> = ({
iconType="cross"
>
<p>
{indexPatternErrorMessage}
{needsDestIndexPattern ? (
<IndexPatternPrompt destIndex={destIndex} color="text" />
) : null}
{dataViewErrorMessage}
{needsDestDataView ? <DataViewPrompt destIndex={destIndex} color="text" /> : null}
</p>
</EuiCallOut>
</EuiPanel>
@ -170,7 +168,7 @@ export const ExplorationPageWrapper: FC<Props> = ({
</>
)}
{indexPattern !== undefined && jobConfig && (
{dataView !== undefined && jobConfig && (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
@ -178,7 +176,7 @@ export const ExplorationPageWrapper: FC<Props> = ({
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<ExplorationQueryBar
indexPattern={indexPattern}
dataView={dataView}
setSearchQuery={searchQueryUpdateHandler}
query={query}
filters={getFilters(jobConfig.dest.results_field!)}
@ -227,7 +225,7 @@ export const ExplorationPageWrapper: FC<Props> = ({
<ExpandableSectionSplom
fields={scatterplotFieldOptions}
index={jobConfig?.dest.index}
indexPattern={indexPattern}
dataView={dataView}
color={
jobType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
@ -242,13 +240,13 @@ export const ExplorationPageWrapper: FC<Props> = ({
{isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />}
{isLoadingJobConfig === false &&
jobConfig !== undefined &&
indexPattern !== undefined &&
dataView !== undefined &&
isInitialized === true && (
<ExplorationResultsTable
indexPattern={indexPattern}
dataView={dataView}
jobConfig={jobConfig}
jobStatus={jobStatus}
needsDestIndexPattern={needsDestIndexPattern}
needsDestDataView={needsDestDataView}
searchQuery={searchQuery}
/>
)}

View file

@ -25,7 +25,7 @@ import { removeFilterFromQueryString } from '../../../../../explorer/explorer_ut
import { useMlKibana } from '../../../../../contexts/kibana';
export interface ExplorationQueryBarProps {
indexPattern: DataView;
dataView: DataView;
setSearchQuery: (update: {
queryString: string;
query?: estypes.QueryDslQueryContainer;
@ -41,7 +41,7 @@ export interface ExplorationQueryBarProps {
}
export const ExplorationQueryBar: FC<ExplorationQueryBarProps> = ({
indexPattern,
dataView,
setSearchQuery,
filters,
query,
@ -99,7 +99,7 @@ export const ExplorationQueryBar: FC<ExplorationQueryBarProps> = ({
case SEARCH_QUERY_LANGUAGE.KUERY:
convertedQuery = toElasticsearchQuery(
fromKueryExpression(query.query as string),
indexPattern
dataView
);
break;
case SEARCH_QUERY_LANGUAGE.LUCENE:
@ -181,7 +181,7 @@ export const ExplorationQueryBar: FC<ExplorationQueryBarProps> = ({
<QueryStringInput
bubbleSubmitEvent={false}
query={searchInput}
indexPatterns={[indexPattern]}
indexPatterns={[dataView]}
onChange={searchChangeHandler}
onSubmit={searchSubmitHandler}
placeholder={

View file

@ -23,15 +23,15 @@ import { ExpandableSectionResults } from '../expandable_section';
import { useExplorationResults } from './use_exploration_results';
interface Props {
indexPattern: DataView;
dataView: DataView;
jobConfig: DataFrameAnalyticsConfig;
jobStatus?: DataFrameTaskStateType;
needsDestIndexPattern: boolean;
needsDestDataView: boolean;
searchQuery: ResultsSearchQuery;
}
export const ExplorationResultsTable: FC<Props> = React.memo(
({ indexPattern, jobConfig, needsDestIndexPattern, searchQuery }) => {
({ dataView, jobConfig, needsDestDataView, searchQuery }) => {
const {
services: {
mlServices: { mlApiServices },
@ -39,7 +39,7 @@ export const ExplorationResultsTable: FC<Props> = React.memo(
} = useMlKibana();
const classificationData = useExplorationResults(
indexPattern,
dataView,
jobConfig,
searchQuery,
getToastNotifications(),
@ -54,10 +54,10 @@ export const ExplorationResultsTable: FC<Props> = React.memo(
<div data-test-subj="mlDFAnalyticsExplorationTablePanel">
<ExpandableSectionResults
indexData={classificationData}
indexPattern={indexPattern}
dataView={dataView}
resultsField={jobConfig?.dest.results_field}
jobConfig={jobConfig}
needsDestIndexPattern={needsDestIndexPattern}
needsDestDataView={needsDestDataView}
searchQuery={searchQuery}
/>
</div>

View file

@ -43,7 +43,7 @@ import { useTrainedModelsApiService } from '../../../../../services/ml_api_servi
import { useExplorationDataGrid } from './use_exploration_data_grid';
export const useExplorationResults = (
indexPattern: DataView | undefined,
dataView: DataView | undefined,
jobConfig: DataFrameAnalyticsConfig | undefined,
searchQuery: estypes.QueryDslQueryContainer,
toastNotifications: CoreSetup['notifications']['toasts'],
@ -54,7 +54,7 @@ export const useExplorationResults = (
const trainedModelsApiService = useTrainedModelsApiService();
const needsDestIndexFields =
indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0];
dataView !== undefined && dataView.title === jobConfig?.source.index[0];
const columns: EuiDataGridColumn[] = [];
@ -90,10 +90,9 @@ export const useExplorationResults = (
}, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]);
const dataLoader = useMemo(
() =>
indexPattern !== undefined ? new DataLoader(indexPattern, toastNotifications) : undefined,
() => (dataView !== undefined ? new DataLoader(dataView, toastNotifications) : undefined),
// eslint-disable-next-line react-hooks/exhaustive-deps
[indexPattern]
[dataView]
);
const fetchColumnChartsData = async function () {
@ -179,7 +178,7 @@ export const useExplorationResults = (
const resultsField = jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD;
const renderCellValue = useRenderCellValue(
indexPattern,
dataView,
dataGrid.pagination,
dataGrid.tableItems,
resultsField

View file

@ -37,7 +37,7 @@ export const JobConfigErrorCallout: FC<Props> = ({
application: { getUrlForApp },
},
} = useMlKibana();
const containsIndexPatternLink =
const containsDataViewLink =
typeof jobCapsServiceErrorMessage === 'string' &&
jobCapsServiceErrorMessage.includes('locate that index-pattern') &&
jobCapsServiceErrorMessage.includes('click here to re-create');
@ -45,7 +45,7 @@ export const JobConfigErrorCallout: FC<Props> = ({
const message = (
<p>{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}</p>
);
const newIndexPatternUrl = useMemo(
const newDataViewUrl = useMemo(
() =>
getUrlForApp('management', {
path: 'kibana/indexPatterns',
@ -54,8 +54,8 @@ export const JobConfigErrorCallout: FC<Props> = ({
[]
);
const calloutBody = containsIndexPatternLink ? (
<EuiLink href={newIndexPatternUrl} target="_blank">
const calloutBody = containsDataViewLink ? (
<EuiLink href={newDataViewUrl} target="_blank">
{message}
</EuiLink>
) : (

View file

@ -33,7 +33,7 @@ import { getFeatureCount } from './common';
import { useOutlierData } from './use_outlier_data';
import { useExplorationUrlState } from '../../hooks/use_exploration_url_state';
import { ExplorationQueryBarProps } from '../exploration_query_bar/exploration_query_bar';
import { IndexPatternPrompt } from '../index_pattern_prompt';
import { DataViewPrompt } from '../data_view_prompt';
export type TableItem = Record<string, any>;
@ -42,12 +42,12 @@ interface ExplorationProps {
}
export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) => {
const { indexPattern, indexPatternErrorMessage, jobConfig, needsDestIndexPattern } =
const { dataView, dataViewErrorMessage, jobConfig, needsDestDataView } =
useResultsViewConfig(jobId);
const [pageUrlState, setPageUrlState] = useExplorationUrlState();
const [searchQuery, setSearchQuery] =
useState<estypes.QueryDslQueryContainer>(defaultSearchQuery);
const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery);
const outlierData = useOutlierData(dataView, jobConfig, searchQuery);
const searchQueryUpdateHandler: ExplorationQueryBarProps['setSearchQuery'] = useCallback(
(update) => {
@ -81,20 +81,20 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) =
// If feature influence was enabled for the legacy job we'll show a callout
// with some additional information for a workaround.
const showLegacyFeatureInfluenceFormatCallout =
!needsDestIndexPattern &&
!needsDestDataView &&
isOutlierAnalysis(jobConfig?.analysis) &&
jobConfig?.analysis.outlier_detection.compute_feature_influence === true &&
columnsWithCharts.findIndex((d) => d.id === `${resultsField}.${FEATURE_INFLUENCE}`) === -1;
const scatterplotFieldOptions = useScatterplotFieldOptions(
indexPattern,
dataView,
jobConfig?.analyzed_fields?.includes,
jobConfig?.analyzed_fields?.excludes,
resultsField
);
const destIndex = getDestinationIndex(jobConfig);
if (indexPatternErrorMessage !== undefined) {
if (dataViewErrorMessage !== undefined) {
return (
<EuiPanel grow={false} hasShadow={false} hasBorder>
<EuiCallOut
@ -105,10 +105,8 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) =
iconType="cross"
>
<p>
{indexPatternErrorMessage}
{needsDestIndexPattern ? (
<IndexPatternPrompt destIndex={destIndex} color="text" />
) : null}
{dataViewErrorMessage}
{needsDestDataView ? <DataViewPrompt destIndex={destIndex} color="text" /> : null}
</p>
</EuiCallOut>
</EuiPanel>
@ -124,10 +122,10 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) =
</>
)}
{(columnsWithCharts.length > 0 || searchQuery !== defaultSearchQuery) &&
indexPattern !== undefined && (
dataView !== undefined && (
<>
<ExplorationQueryBar
indexPattern={indexPattern}
dataView={dataView}
setSearchQuery={searchQueryUpdateHandler}
query={query}
/>
@ -165,9 +163,9 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) =
showColorRange && !showLegacyFeatureInfluenceFormatCallout ? colorRange : undefined
}
indexData={outlierData}
indexPattern={indexPattern}
dataView={dataView}
jobConfig={jobConfig}
needsDestIndexPattern={needsDestIndexPattern}
needsDestDataView={needsDestDataView}
searchQuery={searchQuery}
/>
</>

View file

@ -41,17 +41,17 @@ import { getFeatureCount, getOutlierScoreFieldName } from './common';
import { useExplorationDataGrid } from '../exploration_results_table/use_exploration_data_grid';
export const useOutlierData = (
indexPattern: DataView | undefined,
dataView: DataView | undefined,
jobConfig: DataFrameAnalyticsConfig | undefined,
searchQuery: estypes.QueryDslQueryContainer
): UseIndexDataReturnType => {
const needsDestIndexFields =
indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0];
dataView !== undefined && dataView.title === jobConfig?.source.index[0];
const columns = useMemo(() => {
const newColumns: EuiDataGridColumn[] = [];
if (jobConfig !== undefined && indexPattern !== undefined) {
if (jobConfig !== undefined && dataView !== undefined) {
const resultsField = jobConfig.dest.results_field;
const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields);
newColumns.push(
@ -63,7 +63,7 @@ export const useOutlierData = (
return newColumns;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [jobConfig, indexPattern]);
}, [jobConfig, dataView]);
const dataGrid = useExplorationDataGrid(
columns,
@ -95,11 +95,8 @@ export const useOutlierData = (
}, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]);
const dataLoader = useMemo(
() =>
indexPattern !== undefined
? new DataLoader(indexPattern, getToastNotifications())
: undefined,
[indexPattern]
() => (dataView !== undefined ? new DataLoader(dataView, getToastNotifications()) : undefined),
[dataView]
);
const fetchColumnChartsData = async function () {
@ -146,7 +143,7 @@ export const useOutlierData = (
);
const renderCellValue = useRenderCellValue(
indexPattern,
dataView,
dataGrid.pagination,
dataGrid.tableItems,
jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD,

View file

@ -21,12 +21,12 @@ export const DeleteActionModal: FC<DeleteAction> = ({
closeModal,
deleteAndCloseModal,
deleteTargetIndex,
deleteIndexPattern,
indexPatternExists,
deleteDataView,
dataViewExists,
isLoading,
item,
toggleDeleteIndex,
toggleDeleteIndexPattern,
toggleDeleteDataView,
userCanDeleteIndex,
userCanDeleteDataView,
}) => {
@ -77,15 +77,15 @@ export const DeleteActionModal: FC<DeleteAction> = ({
)}
</EuiFlexItem>
<EuiFlexItem>
{userCanDeleteIndex && indexPatternExists && (
{userCanDeleteIndex && dataViewExists && (
<EuiSwitch
data-test-subj="mlAnalyticsJobDeleteIndexPatternSwitch"
data-test-subj="mlAnalyticsJobDeleteDataViewSwitch"
label={i18n.translate('xpack.ml.dataframe.analyticsList.deleteTargetDataViewTitle', {
defaultMessage: 'Delete data view {dataView}',
values: { dataView: indexName },
})}
checked={deleteIndexPattern}
onChange={toggleDeleteIndexPattern}
checked={deleteDataView}
onChange={toggleDeleteDataView}
disabled={userCanDeleteDataView === false}
/>
)}

View file

@ -99,7 +99,7 @@ describe('DeleteAction', () => {
fireEvent.click(deleteButton);
expect(getByTestId('mlAnalyticsJobDeleteModal')).toBeInTheDocument();
expect(queryByTestId('mlAnalyticsJobDeleteIndexSwitch')).toBeNull();
expect(queryByTestId('mlAnalyticsJobDeleteIndexPatternSwitch')).toBeNull();
expect(queryByTestId('mlAnalyticsJobDeleteDataViewSwitch')).toBeNull();
mock.mockRestore();
});

View file

@ -42,10 +42,10 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
const [isDeleteJobCheckModalVisible, setDeleteJobCheckModalVisible] = useState<boolean>(false);
const [deleteItem, setDeleteItem] = useState(false);
const [deleteTargetIndex, setDeleteTargetIndex] = useState<boolean>(true);
const [deleteIndexPattern, setDeleteIndexPattern] = useState<boolean>(true);
const [deleteDataView, setDeleteDataView] = useState<boolean>(true);
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState<boolean>(false);
const [userCanDeleteDataView, setUserCanDeleteDataView] = useState<boolean>(false);
const [indexPatternExists, setIndexPatternExists] = useState<boolean>(false);
const [dataViewExists, setDataViewExists] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const {
@ -57,13 +57,13 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
const toastNotificationService = useToastNotificationService();
const checkIndexPatternExists = async () => {
const checkDataViewExists = async () => {
try {
const dv = (await dataViews.getIdsWithTitle()).find(({ title }) => title === indexName);
if (dv !== undefined) {
setIndexPatternExists(true);
setDataViewExists(true);
} else {
setIndexPatternExists(false);
setDataViewExists(false);
}
setIsLoading(false);
} catch (e) {
@ -93,7 +93,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
capabilities.indexPatterns.save === true;
setUserCanDeleteDataView(canDeleteDataView);
if (canDeleteDataView === false) {
setDeleteIndexPattern(false);
setDeleteDataView(false);
}
} catch (e) {
const error = extractErrorMessage(e);
@ -116,7 +116,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
setIsLoading(true);
// Check if a data view exists corresponding to current DFA job
// if data view does exist, show it to user
checkIndexPatternExists();
checkDataViewExists();
// Check if an user has permission to delete the index & data view
checkUserIndexPermission();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -129,12 +129,12 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
setModalVisible(false);
if (item !== undefined) {
if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) {
if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteDataView)) {
deleteAnalyticsAndDestIndex(
item.config,
item.stats,
deleteTargetIndex,
indexPatternExists && deleteIndexPattern,
dataViewExists && deleteDataView,
toastNotificationService
);
} else {
@ -143,7 +143,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
}
};
const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex);
const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern);
const toggleDeleteDataView = () => setDeleteDataView(!deleteDataView);
const openModal = (newItem: DataFrameAnalyticsListRowEssentials) => {
setItem(newItem);
@ -181,9 +181,9 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
closeModal,
deleteAndCloseModal,
deleteTargetIndex,
deleteIndexPattern,
deleteDataView,
deleteItem,
indexPatternExists,
dataViewExists,
isDeleteJobCheckModalVisible,
isModalVisible,
isLoading,
@ -192,7 +192,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
openModal,
openDeleteJobCheckModal,
toggleDeleteIndex,
toggleDeleteIndexPattern,
toggleDeleteDataView,
userCanDeleteIndex,
userCanDeleteDataView,
};

View file

@ -146,7 +146,7 @@ export const SourceSelection: FC = () => {
type: 'index-pattern',
getIconForSavedObject: () => 'indexPatternApp',
name: i18n.translate(
'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern',
'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.dataView',
{
defaultMessage: 'Data view',
}

View file

@ -18,7 +18,7 @@ export enum ACTION {
RESET_FORM,
SET_ADVANCED_EDITOR_RAW_STRING,
SET_FORM_STATE,
SET_INDEX_PATTERN_TITLES,
SET_DATA_VIEW_TITLES,
SET_IS_JOB_CREATED,
SET_IS_JOB_STARTED,
SET_IS_MODAL_BUTTON_DISABLED,
@ -51,9 +51,9 @@ export type Action =
}
| { type: ACTION.SET_FORM_STATE; payload: Partial<State['form']> }
| {
type: ACTION.SET_INDEX_PATTERN_TITLES;
type: ACTION.SET_DATA_VIEW_TITLES;
payload: {
indexPatternsMap: SourceIndexMap;
dataViewsMap: SourceIndexMap;
};
}
| { type: ACTION.SET_IS_JOB_CREATED; isJobCreated: State['isJobCreated'] }

View file

@ -39,7 +39,7 @@ const getMockState = ({
jobIdEmpty: false,
jobIdValid: true,
jobIdExists: false,
createIndexPattern: false,
createDataView: false,
},
jobConfig: {
source: { index },

View file

@ -148,7 +148,7 @@ export const validateNumTopFeatureImportanceValues = (
};
export const validateAdvancedEditor = (state: State): State => {
const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern } = state.form;
const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createDataView } = state.form;
const { jobConfig } = state;
state.advancedEditorMessages = [];
@ -161,8 +161,7 @@ export const validateAdvancedEditor = (state: State): State => {
const destinationIndexName = jobConfig?.dest?.index ?? '';
const destinationIndexNameEmpty = destinationIndexName === '';
const destinationIndexNameValid = isValidIndexName(destinationIndexName);
const destinationIndexPatternTitleExists =
state.indexPatternsMap[destinationIndexName] !== undefined;
const destinationDataViewTitleExists = state.dataViewsMap[destinationIndexName] !== undefined;
const analyzedFields = jobConfig?.analyzed_fields?.includes || [];
@ -294,7 +293,7 @@ export const validateAdvancedEditor = (state: State): State => {
),
message: '',
});
} else if (destinationIndexPatternTitleExists && !createIndexPattern) {
} else if (destinationDataViewTitleExists && !createDataView) {
state.advancedEditorMessages.push({
error: i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameExistsWarn',
@ -360,7 +359,7 @@ export const validateAdvancedEditor = (state: State): State => {
});
}
state.form.destinationIndexPatternTitleExists = destinationIndexPatternTitleExists;
state.form.destinationDataViewTitleExists = destinationDataViewTitleExists;
state.isValid =
includesValid &&
@ -377,7 +376,7 @@ export const validateAdvancedEditor = (state: State): State => {
!dependentVariableEmpty &&
!modelMemoryLimitEmpty &&
(numTopFeatureImportanceValuesValid || jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) &&
(!destinationIndexPatternTitleExists || !createIndexPattern);
(!destinationDataViewTitleExists || !createDataView);
return state;
};
@ -425,8 +424,8 @@ const validateForm = (state: State): State => {
sourceIndexNameValid,
destinationIndexNameEmpty,
destinationIndexNameValid,
destinationIndexPatternTitleExists,
createIndexPattern,
destinationDataViewTitleExists,
createDataView,
dependentVariable,
modelMemoryLimit,
numTopFeatureImportanceValuesValid,
@ -458,7 +457,7 @@ const validateForm = (state: State): State => {
destinationIndexNameValid &&
!dependentVariableEmpty &&
(numTopFeatureImportanceValuesValid || jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) &&
(!destinationIndexPatternTitleExists || !createIndexPattern);
(!destinationDataViewTitleExists || !createDataView);
return state;
};
@ -513,8 +512,8 @@ export function reducer(state: State, action: Action): State {
if (action.payload.destinationIndex !== undefined) {
newFormState.destinationIndexNameEmpty = newFormState.destinationIndex === '';
newFormState.destinationIndexNameValid = isValidIndexName(newFormState.destinationIndex);
newFormState.destinationIndexPatternTitleExists =
state.indexPatternsMap[newFormState.destinationIndex] !== undefined;
newFormState.destinationDataViewTitleExists =
state.dataViewsMap[newFormState.destinationIndex] !== undefined;
}
if (action.payload.jobId !== undefined) {
@ -541,13 +540,13 @@ export function reducer(state: State, action: Action): State {
? validateAdvancedEditor({ ...state, form: newFormState })
: validateForm({ ...state, form: newFormState });
case ACTION.SET_INDEX_PATTERN_TITLES: {
case ACTION.SET_DATA_VIEW_TITLES: {
const newState = {
...state,
...action.payload,
};
newState.form.destinationIndexPatternTitleExists =
newState.indexPatternsMap[newState.form.destinationIndex] !== undefined;
newState.form.destinationDataViewTitleExists =
newState.dataViewsMap[newState.form.destinationIndex] !== undefined;
return newState;
}
@ -591,8 +590,8 @@ export function reducer(state: State, action: Action): State {
formState.destinationIndexNameEmpty = formState.destinationIndex === '';
formState.destinationIndexNameValid = isValidIndexName(formState.destinationIndex || '');
formState.destinationIndexPatternTitleExists =
state.indexPatternsMap[formState.destinationIndex || ''] !== undefined;
formState.destinationDataViewTitleExists =
state.dataViewsMap[formState.destinationIndex || ''] !== undefined;
if (formState.numTopFeatureImportanceValues !== undefined) {
formState.numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues(

View file

@ -35,13 +35,10 @@ export const UNSET_CONFIG_ITEM = '--';
export type EsIndexName = string;
export type DependentVariable = string;
export type IndexPatternTitle = string;
export type DataViewTitle = string;
export type AnalyticsJobType = DataFrameAnalysisConfigType | undefined;
type IndexPatternId = string;
export type SourceIndexMap = Record<
IndexPatternTitle,
{ label: IndexPatternTitle; value: IndexPatternId }
>;
type DataViewId = string;
export type SourceIndexMap = Record<DataViewTitle, { label: DataViewTitle; value: DataViewId }>;
export interface FormMessage {
error?: string;
@ -55,7 +52,7 @@ export interface State {
form: {
alpha: undefined | number;
computeFeatureInfluence: string;
createIndexPattern: boolean;
createDataView: boolean;
classAssignmentObjective: undefined | string;
dependentVariable: DependentVariable;
description: string;
@ -63,7 +60,7 @@ export interface State {
destinationIndexNameExists: boolean;
destinationIndexNameEmpty: boolean;
destinationIndexNameValid: boolean;
destinationIndexPatternTitleExists: boolean;
destinationDataViewTitleExists: boolean;
downsampleFactor: undefined | number;
earlyStoppingEnabled: undefined | boolean;
eta: undefined | number;
@ -120,7 +117,7 @@ export interface State {
useEstimatedMml: boolean;
};
disabled: boolean;
indexPatternsMap: SourceIndexMap;
dataViewsMap: SourceIndexMap;
isAdvancedEditorEnabled: boolean;
isAdvancedEditorValidJson: boolean;
hasSwitchedToEditor: boolean;
@ -141,7 +138,7 @@ export const getInitialState = (): State => ({
form: {
alpha: undefined,
computeFeatureInfluence: 'true',
createIndexPattern: true,
createDataView: true,
classAssignmentObjective: undefined,
dependentVariable: '',
description: '',
@ -149,7 +146,7 @@ export const getInitialState = (): State => ({
destinationIndexNameExists: false,
destinationIndexNameEmpty: true,
destinationIndexNameValid: false,
destinationIndexPatternTitleExists: false,
destinationDataViewTitleExists: false,
earlyStoppingEnabled: undefined,
downsampleFactor: undefined,
eta: undefined,
@ -210,7 +207,7 @@ export const getInitialState = (): State => ({
!mlNodesAvailable() ||
!checkPermission('canCreateDataFrameAnalytics') ||
!checkPermission('canStartStopDataFrameAnalytics'),
indexPatternsMap: {},
dataViewsMap: {},
isAdvancedEditorEnabled: false,
isAdvancedEditorValidJson: true,
hasSwitchedToEditor: false,

View file

@ -59,7 +59,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
const { refresh } = useRefreshAnalyticsList();
const { form, jobConfig, isAdvancedEditorEnabled } = state;
const { createIndexPattern, jobId } = form;
const { createDataView, jobId } = form;
let { destinationIndex } = form;
const addRequestMessage = (requestMessage: FormMessage) =>
@ -73,8 +73,8 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
const setAdvancedEditorRawString = (advancedEditorRawString: string) =>
dispatch({ type: ACTION.SET_ADVANCED_EDITOR_RAW_STRING, advancedEditorRawString });
const setIndexPatternTitles = (payload: { indexPatternsMap: SourceIndexMap }) =>
dispatch({ type: ACTION.SET_INDEX_PATTERN_TITLES, payload });
const setDataViewTitles = (payload: { dataViewsMap: SourceIndexMap }) =>
dispatch({ type: ACTION.SET_DATA_VIEW_TITLES, payload });
const setIsJobCreated = (isJobCreated: boolean) =>
dispatch({ type: ACTION.SET_IS_JOB_CREATED, isJobCreated });
@ -110,7 +110,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
),
});
setIsJobCreated(true);
if (createIndexPattern) {
if (createDataView) {
createKibanaDataView(destinationIndex, dataViews, form.timeFieldName, addRequestMessage);
}
refresh();
@ -132,17 +132,17 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
const prepareFormValidation = async () => {
try {
// Set the existing data view names.
const indexPatternsMap: SourceIndexMap = {};
const dataViewsMap: SourceIndexMap = {};
const savedObjects = (await dataViews.getCache()) || [];
savedObjects.forEach((obj) => {
const title = obj?.attributes?.title;
if (title !== undefined) {
const id = obj?.id || '';
indexPatternsMap[title] = { label: title, value: id };
dataViewsMap[title] = { label: title, value: id };
}
});
setIndexPatternTitles({
indexPatternsMap,
setDataViewTitles({
dataViewsMap,
});
} catch (e) {
addRequestMessage({

View file

@ -48,7 +48,7 @@ export const deleteAnalyticsAndDestIndex = async (
analyticsConfig: DataFrameAnalyticsListRow['config'],
analyticsStats: DataFrameAnalyticsListRow['stats'],
deleteDestIndex: boolean,
deleteDestIndexPattern: boolean,
deleteDestDataView: boolean,
toastNotificationService: ToastNotificationService
) => {
const destinationIndex = analyticsConfig.dest.index;
@ -59,7 +59,7 @@ export const deleteAnalyticsAndDestIndex = async (
const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex(
analyticsConfig.id,
deleteDestIndex,
deleteDestIndexPattern
deleteDestDataView
);
if (status.analyticsJobDeleted?.success) {
toastNotificationService.displaySuccessToast(
@ -97,7 +97,7 @@ export const deleteAnalyticsAndDestIndex = async (
);
}
if (status.destIndexPatternDeleted?.success) {
if (status.destDataViewDeleted?.success) {
toastNotificationService.displaySuccessToast(
i18n.translate(
'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewSuccessMessage',
@ -108,8 +108,8 @@ export const deleteAnalyticsAndDestIndex = async (
)
);
}
if (status.destIndexPatternDeleted?.error) {
const error = extractErrorMessage(status.destIndexPatternDeleted.error);
if (status.destDataViewDeleted?.error) {
const error = extractErrorMessage(status.destDataViewDeleted.error);
toastNotificationService.displayDangerToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewErrorMessage', {
defaultMessage: 'An error occurred deleting data view {destinationIndex}: {error}',

View file

@ -49,7 +49,7 @@ export interface DeleteDataFrameAnalyticsWithIndexResponse {
acknowledged: boolean;
analyticsJobDeleted: DeleteDataFrameAnalyticsWithIndexStatus;
destIndexDeleted: DeleteDataFrameAnalyticsWithIndexStatus;
destIndexPatternDeleted: DeleteDataFrameAnalyticsWithIndexStatus;
destDataViewDeleted: DeleteDataFrameAnalyticsWithIndexStatus;
}
export interface JobsExistsResponse {
@ -152,11 +152,11 @@ export const dataFrameAnalyticsApiProvider = (httpService: HttpService) => ({
deleteDataFrameAnalyticsAndDestIndex(
analyticsId: string,
deleteDestIndex: boolean,
deleteDestIndexPattern: boolean
deleteDestDataView: boolean
) {
return httpService.http<DeleteDataFrameAnalyticsWithIndexResponse>({
path: `${ML_INTERNAL_BASE_PATH}/data_frame/analytics/${analyticsId}`,
query: { deleteDestIndex, deleteDestIndexPattern },
query: { deleteDestIndex, deleteDestDataView },
method: 'DELETE',
version: '1',
});

View file

@ -129,6 +129,6 @@ export function timeBasedIndexCheck(dataView: DataView, showNotification = false
* Returns true if the data view index pattern contains a :
* which means it is cross-cluster
*/
export function isCcsIndexPattern(dataViewIndexPattern: string) {
return dataViewIndexPattern.includes(':');
export function isCcsIndexPattern(indexPattern: string) {
return indexPattern.includes(':');
}

View file

@ -9,7 +9,7 @@ import { DataViewsService } from '@kbn/data-views-plugin/common';
export class DataViewHandler {
constructor(private dataViewService: DataViewsService) {}
// returns a id based on an index pattern name
// returns a id based on an data view name
async getDataViewId(indexName: string) {
const dv = (await this.dataViewService.find(indexName)).find(
({ title }) => title === indexName

View file

@ -18,35 +18,35 @@ import { wrapError } from '../client/error_wrapper';
import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages';
import type { RouteInitialization } from '../types';
import {
dataAnalyticsJobConfigSchema,
dataAnalyticsJobUpdateSchema,
dataAnalyticsEvaluateSchema,
dataAnalyticsExplainSchema,
analyticsIdSchema,
analyticsMapQuerySchema,
dataFrameAnalyticsJobConfigSchema,
dataFrameAnalyticsJobUpdateSchema,
dataFrameAnalyticsEvaluateSchema,
dataFrameAnalyticsExplainSchema,
dataFrameAnalyticsIdSchema,
dataFrameAnalyticsMapQuerySchema,
stopsDataFrameAnalyticsJobQuerySchema,
deleteDataFrameAnalyticsJobSchema,
jobsExistSchema,
analyticsQuerySchema,
analyticsNewJobCapsParamsSchema,
analyticsNewJobCapsQuerySchema,
} from './schemas/data_analytics_schema';
dataFrameAnalyticsJobsExistSchema,
dataFrameAnalyticsQuerySchema,
dataFrameAnalyticsNewJobCapsParamsSchema,
dataFrameAnalyticsNewJobCapsQuerySchema,
} from './schemas/data_frame_analytics_schema';
import type { ExtendAnalyticsMapArgs } from '../models/data_frame_analytics/types';
import { DataViewHandler } from '../models/data_frame_analytics/index_patterns';
import { DataViewHandler } from '../models/data_frame_analytics/data_view_handler';
import { AnalyticsManager } from '../models/data_frame_analytics/analytics_manager';
import { validateAnalyticsJob } from '../models/data_frame_analytics/validation';
import { fieldServiceProvider } from '../models/job_service/new_job_caps/field_service';
import { getAuthorizationHeader } from '../lib/request_authorization';
import type { MlClient } from '../lib/ml_client';
function getDataViewId(dataViewsService: DataViewsService, patternName: string) {
async function getDataViewId(dataViewsService: DataViewsService, patternName: string) {
const iph = new DataViewHandler(dataViewsService);
return iph.getDataViewId(patternName);
return await iph.getDataViewId(patternName);
}
function deleteDestDataViewById(dataViewsService: DataViewsService, dataViewId: string) {
async function deleteDestDataViewById(dataViewsService: DataViewsService, dataViewId: string) {
const iph = new DataViewHandler(dataViewsService);
return iph.deleteDataViewById(dataViewId);
return await iph.deleteDataViewById(dataViewId);
}
function getExtendedMap(
@ -144,7 +144,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
query: analyticsQuerySchema,
query: dataFrameAnalyticsQuerySchema,
},
},
},
@ -185,8 +185,8 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
query: analyticsQuerySchema,
params: dataFrameAnalyticsIdSchema,
query: dataFrameAnalyticsQuerySchema,
},
},
},
@ -262,7 +262,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
params: dataFrameAnalyticsIdSchema,
},
},
},
@ -305,8 +305,8 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
body: dataAnalyticsJobConfigSchema,
params: dataFrameAnalyticsIdSchema,
body: dataFrameAnalyticsJobConfigSchema,
},
},
},
@ -352,7 +352,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
body: dataAnalyticsEvaluateSchema,
body: dataFrameAnalyticsEvaluateSchema,
},
},
},
@ -397,7 +397,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
body: dataAnalyticsExplainSchema,
body: dataFrameAnalyticsExplainSchema,
},
},
},
@ -440,7 +440,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
params: dataFrameAnalyticsIdSchema,
query: deleteDataFrameAnalyticsJobSchema,
},
},
@ -449,11 +449,11 @@ export function dataFrameAnalyticsRoutes(
async ({ mlClient, client, request, response, getDataViewsService }) => {
try {
const { analyticsId } = request.params;
const { deleteDestIndex, deleteDestIndexPattern } = request.query;
const { deleteDestIndex, deleteDestDataView } = request.query;
let destinationIndex: string | undefined;
const analyticsJobDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { success: false };
const destIndexDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { success: false };
const destIndexPatternDeleted: DeleteDataFrameAnalyticsWithIndexStatus = {
const destDataViewDeleted: DeleteDataFrameAnalyticsWithIndexStatus = {
success: false,
};
@ -473,7 +473,7 @@ export function dataFrameAnalyticsRoutes(
return response.customError(wrapError(e));
}
if (deleteDestIndex || deleteDestIndexPattern) {
if (deleteDestIndex || deleteDestDataView) {
// If user checks box to delete the destinationIndex associated with the job
if (destinationIndex && deleteDestIndex) {
// Verify if user has privilege to delete the destination index
@ -494,16 +494,16 @@ export function dataFrameAnalyticsRoutes(
}
// Delete the index pattern if there's an index pattern that matches the name of dest index
if (destinationIndex && deleteDestIndexPattern) {
if (destinationIndex && deleteDestDataView) {
try {
const dataViewsService = await getDataViewsService();
const dataViewId = await getDataViewId(dataViewsService, destinationIndex);
if (dataViewId) {
await deleteDestDataViewById(dataViewsService, dataViewId);
}
destIndexPatternDeleted.success = true;
destDataViewDeleted.success = true;
} catch (deleteDestIndexPatternError) {
destIndexPatternDeleted.error = deleteDestIndexPatternError;
destDataViewDeleted.error = deleteDestIndexPatternError;
}
}
}
@ -521,7 +521,7 @@ export function dataFrameAnalyticsRoutes(
const results = {
analyticsJobDeleted,
destIndexDeleted,
destIndexPatternDeleted,
destDataViewDeleted,
};
return response.ok({
body: results,
@ -555,7 +555,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
params: dataFrameAnalyticsIdSchema,
},
},
},
@ -597,7 +597,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
params: dataFrameAnalyticsIdSchema,
query: stopsDataFrameAnalyticsJobQuerySchema,
},
},
@ -640,8 +640,8 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
body: dataAnalyticsJobUpdateSchema,
params: dataFrameAnalyticsIdSchema,
body: dataFrameAnalyticsJobUpdateSchema,
},
},
},
@ -686,7 +686,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
params: dataFrameAnalyticsIdSchema,
},
},
},
@ -728,7 +728,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
body: jobsExistSchema,
body: dataFrameAnalyticsJobsExistSchema,
},
},
},
@ -785,8 +785,8 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsIdSchema,
query: analyticsMapQuerySchema,
params: dataFrameAnalyticsIdSchema,
query: dataFrameAnalyticsMapQuerySchema,
},
},
},
@ -851,8 +851,8 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
params: analyticsNewJobCapsParamsSchema,
query: analyticsNewJobCapsQuerySchema,
params: dataFrameAnalyticsNewJobCapsParamsSchema,
query: dataFrameAnalyticsNewJobCapsQuerySchema,
},
},
},
@ -906,7 +906,7 @@ export function dataFrameAnalyticsRoutes(
version: '1',
validate: {
request: {
body: dataAnalyticsJobConfigSchema,
body: dataFrameAnalyticsJobConfigSchema,
},
},
},

View file

@ -8,7 +8,7 @@
import { schema } from '@kbn/config-schema';
import { runtimeMappingsSchema } from './runtime_mappings_schema';
export const dataAnalyticsJobConfigSchema = schema.object({
export const dataFrameAnalyticsJobConfigSchema = schema.object({
description: schema.maybe(schema.string()),
_meta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
dest: schema.object({
@ -35,7 +35,7 @@ export const dataAnalyticsJobConfigSchema = schema.object({
max_num_threads: schema.maybe(schema.number()),
});
export const dataAnalyticsEvaluateSchema = schema.object({
export const dataFrameAnalyticsEvaluateSchema = schema.object({
index: schema.string(),
query: schema.maybe(schema.any()),
evaluation: schema.maybe(
@ -47,7 +47,7 @@ export const dataAnalyticsEvaluateSchema = schema.object({
),
});
export const dataAnalyticsExplainSchema = schema.object({
export const dataFrameAnalyticsExplainSchema = schema.object({
description: schema.maybe(schema.string()),
dest: schema.maybe(schema.any()),
/** Source */
@ -63,14 +63,14 @@ export const dataAnalyticsExplainSchema = schema.object({
_meta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
});
export const analyticsIdSchema = schema.object({
export const dataFrameAnalyticsIdSchema = schema.object({
/**
* Analytics ID
*/
analyticsId: schema.string(),
});
export const analyticsQuerySchema = schema.object({
export const dataFrameAnalyticsQuerySchema = schema.object({
/**
* Analytics Query
*/
@ -83,10 +83,10 @@ export const deleteDataFrameAnalyticsJobSchema = schema.object({
* Analytics Destination Index
*/
deleteDestIndex: schema.maybe(schema.boolean()),
deleteDestIndexPattern: schema.maybe(schema.boolean()),
deleteDestDataView: schema.maybe(schema.boolean()),
});
export const dataAnalyticsJobUpdateSchema = schema.object({
export const dataFrameAnalyticsJobUpdateSchema = schema.object({
description: schema.maybe(schema.string()),
model_memory_limit: schema.maybe(schema.string()),
allow_lazy_start: schema.maybe(schema.boolean()),
@ -98,17 +98,19 @@ export const stopsDataFrameAnalyticsJobQuerySchema = schema.object({
force: schema.maybe(schema.boolean()),
});
export const jobsExistSchema = schema.object({
export const dataFrameAnalyticsJobsExistSchema = schema.object({
analyticsIds: schema.arrayOf(schema.string()),
allSpaces: schema.maybe(schema.boolean()),
});
export const analyticsMapQuerySchema = schema.maybe(
export const dataFrameAnalyticsMapQuerySchema = schema.maybe(
schema.object({ treatAsRoot: schema.maybe(schema.any()), type: schema.maybe(schema.string()) })
);
export const analyticsNewJobCapsParamsSchema = schema.object({ indexPattern: schema.string() });
export const dataFrameAnalyticsNewJobCapsParamsSchema = schema.object({
indexPattern: schema.string(),
});
export const analyticsNewJobCapsQuerySchema = schema.maybe(
export const dataFrameAnalyticsNewJobCapsQuerySchema = schema.maybe(
schema.object({ rollup: schema.maybe(schema.string()) })
);

View file

@ -17,7 +17,7 @@ import {
getFieldType,
getDataGridSchemaFromKibanaFieldType,
getDataGridSchemaFromESFieldType,
getFieldsFromKibanaIndexPattern,
getFieldsFromKibanaDataView,
showDataGridColumnChartErrorMessageToast,
useDataGrid,
useRenderCellValue,
@ -140,7 +140,7 @@ export const useIndexData = (
allPopulatedFields = [...new Set(docs.map(Object.keys).flat(1))];
}
const allDataViewFields = getFieldsFromKibanaIndexPattern(dataView);
const allDataViewFields = getFieldsFromKibanaDataView(dataView);
return allPopulatedFields.filter((d) => allDataViewFields.includes(d)).sort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataViewFieldsData, populatedFields]);

View file

@ -25329,7 +25329,7 @@
"xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutTitle": "Les vues de données utilisant la recherche inter-clusters ne sont pas prises en charge.",
"xpack.ml.dataFrame.analytics.create.searchSelection.errorGettingDataViewTitle": "Erreur lors du chargement de la vue de données utilisée par la recherche enregistrée",
"xpack.ml.dataFrame.analytics.create.searchSelection.notFoundLabel": "Aucun recherche enregistrée ni aucun index correspondants n'ont été trouvés.",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern": "Vue de données",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.dataView": "Vue de données",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search": "Recherche enregistrée",
"xpack.ml.dataframe.analytics.create.shouldCreateDataViewMessage": "Vous ne pourrez peut-être pas visualiser les résultats de la tâche si aucune vue de données n'est créée pour l'index de destination.",
"xpack.ml.dataframe.analytics.create.softTreeDepthLimitInputAriaLabel": "Les arbres de décision dépassant cette profondeur sont pénalisés dans les calculs de perte.",

View file

@ -25328,7 +25328,7 @@
"xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutTitle": "クラスター横断検索を使用するデータビューはサポートされていません。",
"xpack.ml.dataFrame.analytics.create.searchSelection.errorGettingDataViewTitle": "保存された検索で使用されているデータビューの読み込みエラー",
"xpack.ml.dataFrame.analytics.create.searchSelection.notFoundLabel": "一致インデックスまたは保存した検索が見つかりません。",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern": "データビュー",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.dataView": "データビュー",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search": "保存検索",
"xpack.ml.dataframe.analytics.create.shouldCreateDataViewMessage": "デスティネーションインデックスのデータビューが作成されていない場合は、ジョブ結果を表示できないことがあります。",
"xpack.ml.dataframe.analytics.create.softTreeDepthLimitInputAriaLabel": "この深さを超える決定木は、損失計算でペナルティがあります。",

View file

@ -25327,7 +25327,7 @@
"xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutTitle": "不支持使用跨集群搜索的数据视图。",
"xpack.ml.dataFrame.analytics.create.searchSelection.errorGettingDataViewTitle": "加载已保存搜索所使用的数据视图时出错",
"xpack.ml.dataFrame.analytics.create.searchSelection.notFoundLabel": "未找到匹配的索引或已保存搜索。",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern": "数据视图",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.dataView": "数据视图",
"xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search": "已保存搜索",
"xpack.ml.dataframe.analytics.create.shouldCreateDataViewMessage": "如果没有为目标索引创建数据视图,则可能无法查看作业结果。",
"xpack.ml.dataframe.analytics.create.softTreeDepthLimitInputAriaLabel": "超过此深度的决策树将在损失计算中被罚分。",

View file

@ -12,6 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./containers'));
loadTestFile(require.resolve('./hosts'));
loadTestFile(require.resolve('./services'));
loadTestFile(require.resolve('./pods'));
loadTestFile(require.resolve('./sample_assets'));
});
}

View file

@ -0,0 +1,100 @@
/*
* 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 { timerange, infra } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { Asset } from '@kbn/assetManager-plugin/common/types_api';
import * as routePaths from '@kbn/assetManager-plugin/common/constants_routes';
import { FtrProviderContext } from '../types';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const synthtrace = getService('infraSynthtraceEsClient');
describe(`GET ${routePaths.GET_PODS}`, () => {
const from = new Date(Date.now() - 1000 * 60 * 2).toISOString();
const to = new Date().toISOString();
beforeEach(async () => {
await synthtrace.clean();
});
it('should return pod assets', async () => {
await synthtrace.index(generatePodsData({ from, to, count: 5 }));
const response = await supertest
.get(routePaths.GET_PODS)
.query({
from,
to,
})
.expect(200);
expect(response.body).to.have.property('pods');
expect(response.body.pods.length).to.equal(5);
});
it('should return a specific pod asset by EAN', async () => {
await synthtrace.index(generatePodsData({ from, to, count: 5 }));
const testEan = 'pod:pod-uid-1';
const response = await supertest
.get(routePaths.GET_PODS)
.query({
from,
to,
stringFilters: JSON.stringify({ ean: testEan }),
})
.expect(200);
expect(response.body).to.have.property('pods');
expect(response.body.pods.length).to.equal(1);
expect(response.body.pods[0]['asset.ean']).to.equal(testEan);
});
it('should return a filtered list of pods assets by ID wildcard pattern', async () => {
await synthtrace.index(generatePodsData({ from, to, count: 15 }));
const testIdPattern = '*id-1*';
const response = await supertest
.get(routePaths.GET_PODS)
.query({
from,
to,
stringFilters: JSON.stringify({ id: testIdPattern }),
})
.expect(200);
expect(response.body).to.have.property('pods');
expect(response.body.pods.length).to.equal(6);
const ids = response.body.pods.map((result: Asset) => result['asset.id'][0]);
expect(ids).to.eql([
'pod-uid-1',
'pod-uid-10',
'pod-uid-11',
'pod-uid-12',
'pod-uid-13',
'pod-uid-14',
]);
});
});
}
function generatePodsData({ from, to, count = 1 }: { from: string; to: string; count: number }) {
const range = timerange(from, to);
const pods = Array(count)
.fill(0)
.map((_, idx) => infra.pod(`pod-uid-${idx}`, `node-name-${idx}`));
return range
.interval('1m')
.rate(1)
.generator((timestamp) => pods.map((pod) => pod.metrics().timestamp(timestamp)));
}

View file

@ -148,13 +148,13 @@ export default ({ getService }: FtrProviderContext) => {
expect(body.analyticsJobDeleted.success).to.eql(true);
expect(body.destIndexDeleted.success).to.eql(true);
expect(body.destIndexPatternDeleted.success).to.eql(false);
expect(body.destDataViewDeleted.success).to.eql(false);
await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId);
await ml.api.assertIndicesNotToExist(destinationIndex);
});
});
describe('with deleteDestIndexPattern setting', function () {
describe('with deleteDestDataView setting', function () {
const analyticsId = `${jobId}_3`;
const destinationIndex = generateDestinationIndex(analyticsId);
@ -170,20 +170,20 @@ export default ({ getService }: FtrProviderContext) => {
it('should delete job and data view by id', async () => {
const { body, status } = await supertest
.delete(`/internal/ml/data_frame/analytics/${analyticsId}`)
.query({ deleteDestIndexPattern: true })
.query({ deleteDestDataView: true })
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(getCommonRequestHeader('1'));
ml.api.assertResponseStatusCode(200, status, body);
expect(body.analyticsJobDeleted.success).to.eql(true);
expect(body.destIndexDeleted.success).to.eql(false);
expect(body.destIndexPatternDeleted.success).to.eql(true);
expect(body.destDataViewDeleted.success).to.eql(true);
await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId);
await ml.testResources.assertDataViewNotExist(destinationIndex);
});
});
describe('with deleteDestIndex & deleteDestIndexPattern setting', function () {
describe('with deleteDestIndex & deleteDestDataView setting', function () {
const analyticsId = `${jobId}_4`;
const destinationIndex = generateDestinationIndex(analyticsId);
@ -202,14 +202,14 @@ export default ({ getService }: FtrProviderContext) => {
it('should delete job, target index, and data view by id', async () => {
const { body, status } = await supertest
.delete(`/internal/ml/data_frame/analytics/${analyticsId}`)
.query({ deleteDestIndex: true, deleteDestIndexPattern: true })
.query({ deleteDestIndex: true, deleteDestDataView: true })
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(getCommonRequestHeader('1'));
ml.api.assertResponseStatusCode(200, status, body);
expect(body.analyticsJobDeleted.success).to.eql(true);
expect(body.destIndexDeleted.success).to.eql(true);
expect(body.destIndexPatternDeleted.success).to.eql(true);
expect(body.destDataViewDeleted.success).to.eql(true);
await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId);
await ml.api.assertIndicesNotToExist(destinationIndex);
await ml.testResources.assertDataViewNotExist(destinationIndex);

View file

@ -17,7 +17,7 @@ import {
const testDiscoverCustomUrl: DiscoverUrlConfig = {
label: 'Show data',
indexPattern: 'ft_bank_marketing',
indexName: 'ft_bank_marketing',
queryEntityFieldNames: ['day'],
timeRange: TIME_RANGE_TYPE.AUTO,
};
@ -76,7 +76,7 @@ export default function ({ getService }: FtrProviderContext) {
dependentVariable: 'y',
trainingPercent: 20,
modelMemory: '60mb',
createIndexPattern: true,
createDataView: true,
fieldStatsEntries: [
{
fieldName: 'age',
@ -339,9 +339,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('sets the create data view switch');
await ml.dataFrameAnalyticsCreation.assertCreateDataViewSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(
testData.createIndexPattern
);
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(testData.createDataView);
});
it('runs the analytics job and displays it correctly in the job list', async () => {

View file

@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) {
const testDataList: Array<{
suiteTitle: string;
archive: string;
indexPattern: { name: string; timeField: string };
dataView: { name: string; timeField: string };
job: DeepPartial<DataFrameAnalyticsConfig>;
}> = (() => {
const timestamp = Date.now();
@ -28,7 +28,7 @@ export default function ({ getService }: FtrProviderContext) {
{
suiteTitle: 'classification job supported by the form',
archive: 'x-pack/test/functional/es_archives/ml/bm_classification',
indexPattern: { name: 'ft_bank_marketing', timeField: '@timestamp' },
dataView: { name: 'ft_bank_marketing', timeField: '@timestamp' },
job: {
id: `bm_1_${timestamp}`,
description:
@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) {
{
suiteTitle: 'outlier detection job supported by the form',
archive: 'x-pack/test/functional/es_archives/ml/ihp_outlier',
indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
dataView: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
job: {
id: `ihp_1_${timestamp}`,
description: 'This is the job description',
@ -92,7 +92,7 @@ export default function ({ getService }: FtrProviderContext) {
{
suiteTitle: 'regression job supported by the form',
archive: 'x-pack/test/functional/es_archives/ml/egs_regression',
indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' },
dataView: { name: 'ft_egs_regression', timeField: '@timestamp' },
job: {
id: `egs_1_${timestamp}`,
description: 'This is the job description',
@ -142,8 +142,8 @@ export default function ({ getService }: FtrProviderContext) {
before(async () => {
await esArchiver.loadIfNeeded(testData.archive);
await ml.testResources.createDataViewIfNeeded(
testData.indexPattern.name,
testData.indexPattern.timeField
testData.dataView.name,
testData.dataView.timeField
);
await ml.api.createDataFrameAnalyticsJob(testData.job as DataFrameAnalyticsConfig);
@ -159,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.api.deleteIndices(testData.job.dest!.index as string);
await ml.testResources.deleteDataViewByTitle(testData.job.dest!.index as string);
await ml.testResources.deleteDataViewByTitle(cloneDestIndex);
await ml.testResources.deleteDataViewByTitle(testData.indexPattern.name);
await ml.testResources.deleteDataViewByTitle(testData.dataView.name);
});
it('opens the existing job in the data frame analytics job wizard', async () => {

View file

@ -15,7 +15,7 @@ import {
const testDiscoverCustomUrl: DiscoverUrlConfig = {
label: 'Show data',
indexPattern: 'ft_farequote',
indexName: 'ft_farequote',
queryEntityFieldNames: ['airline'],
timeRange: TIME_RANGE_TYPE.AUTO,
};

View file

@ -17,7 +17,7 @@ import {
const testDiscoverCustomUrl: DiscoverUrlConfig = {
label: 'Show data',
indexPattern: 'ft_ihp_outlier',
indexName: 'ft_ihp_outlier',
queryEntityFieldNames: ['SaleType'],
timeRange: TIME_RANGE_TYPE.AUTO,
};
@ -83,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) {
},
},
modelMemory: '5mb',
createIndexPattern: true,
createDataView: true,
advancedEditorContent: [
'{',
' "description": "Outlier detection job based on ft_ihp_outlier dataset with runtime fields",',
@ -325,9 +325,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('sets the create data view switch');
await ml.dataFrameAnalyticsCreation.assertCreateDataViewSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(
testData.createIndexPattern
);
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(testData.createDataView);
});
it('runs the analytics job and displays it correctly in the job list', async () => {

View file

@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) {
},
},
modelMemory: '1mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
histogramCharts: [
@ -140,7 +140,7 @@ export default function ({ getService }: FtrProviderContext) {
},
},
modelMemory: '65mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
histogramCharts: [
@ -217,7 +217,7 @@ export default function ({ getService }: FtrProviderContext) {
},
},
modelMemory: '65mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
histogramCharts: [
@ -295,7 +295,7 @@ export default function ({ getService }: FtrProviderContext) {
},
},
modelMemory: '65mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
histogramCharts: [
@ -464,9 +464,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('sets the create data view switch');
await ml.dataFrameAnalyticsCreation.assertCreateDataViewSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(
testData.createIndexPattern
);
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(testData.createDataView);
});
it('runs the analytics job and displays it correctly in the job list', async () => {

View file

@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) {
const testDiscoverCustomUrl: DiscoverUrlConfig = {
label: 'Show data',
indexPattern: 'ft_egs_regression',
indexName: 'ft_egs_regression',
queryEntityFieldNames: ['stabf'],
timeRange: TIME_RANGE_TYPE.AUTO,
};
@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) {
dependentVariable: 'stab',
trainingPercent: 20,
modelMemory: '20mb',
createIndexPattern: true,
createDataView: true,
advancedEditorContent: [
'{',
' "description": "Regression job based on ft_egs_regression dataset with runtime fields",',
@ -340,9 +340,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('sets the create data view switch');
await ml.dataFrameAnalyticsCreation.assertCreateDataViewSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(
testData.createIndexPattern
);
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(testData.createDataView);
});
it('runs the analytics job and displays it correctly in the job list', async () => {

View file

@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) {
dependentVariable: 'responsetime',
trainingPercent: 20,
modelMemory: '20mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
runtimeFieldsEditorContent: ['{', ' "uppercase_airline": {', ' "type": "keyword",'],
@ -161,7 +161,7 @@ export default function ({ getService }: FtrProviderContext) {
dependentVariable: 'responsetime',
trainingPercent: 20,
modelMemory: '20mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
runtimeFieldsEditorContent: ['{', ' "uppercase_airline": {', ' "type": "keyword",'],
@ -249,7 +249,7 @@ export default function ({ getService }: FtrProviderContext) {
dependentVariable: 'responsetime',
trainingPercent: 20,
modelMemory: '20mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
runtimeFieldsEditorContent: ['{', ' "uppercase_airline": {', ' "type": "keyword",'],
@ -331,7 +331,7 @@ export default function ({ getService }: FtrProviderContext) {
dependentVariable: 'responsetime',
trainingPercent: 20,
modelMemory: '20mb',
createIndexPattern: true,
createDataView: true,
expected: {
source: 'ft_farequote_small',
runtimeFieldsEditorContent: ['{', ' "uppercase_airline": {', ' "type": "keyword",'],
@ -499,9 +499,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('sets the create data view switch');
await ml.dataFrameAnalyticsCreation.assertCreateDataViewSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(
testData.createIndexPattern
);
await ml.dataFrameAnalyticsCreation.setCreateDataViewSwitchState(testData.createDataView);
});
it('runs the analytics job and displays it correctly in the job list', async () => {

View file

@ -18,7 +18,7 @@ export default function ({ getService }: FtrProviderContext) {
const testDataList: Array<{
suiteTitle: string;
archive: string;
indexPattern: { name: string; timeField: string };
dataView: { name: string; timeField: string };
job: DeepPartial<DataFrameAnalyticsConfig>;
sortBy: {
column: string;
@ -38,7 +38,7 @@ export default function ({ getService }: FtrProviderContext) {
{
suiteTitle: 'binary classification job',
archive: 'x-pack/test/functional/es_archives/ml/ihp_outlier',
indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
dataView: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
job: {
id: `ihp_fi_binary_${timestamp}`,
description:
@ -107,7 +107,7 @@ export default function ({ getService }: FtrProviderContext) {
{
suiteTitle: 'multi class classification job',
archive: 'x-pack/test/functional/es_archives/ml/ihp_outlier',
indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
dataView: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
job: {
id: `ihp_fi_multi_${timestamp}`,
description:
@ -178,7 +178,7 @@ export default function ({ getService }: FtrProviderContext) {
{
suiteTitle: 'regression job',
archive: 'x-pack/test/functional/es_archives/ml/egs_regression',
indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' },
dataView: { name: 'ft_egs_regression', timeField: '@timestamp' },
job: {
id: `egs_fi_reg_${timestamp}`,
description: 'This is the job description',
@ -253,8 +253,8 @@ export default function ({ getService }: FtrProviderContext) {
for (const testData of testDataList) {
await esArchiver.loadIfNeeded(testData.archive);
await ml.testResources.createDataViewIfNeeded(
testData.indexPattern.name,
testData.indexPattern.timeField
testData.dataView.name,
testData.dataView.timeField
);
await ml.api.createAndRunDFAJob(testData.job as DataFrameAnalyticsConfig);
}
@ -263,7 +263,7 @@ export default function ({ getService }: FtrProviderContext) {
after(async () => {
await ml.api.cleanMlIndices();
for (const testData of testDataList) {
await ml.testResources.deleteDataViewByTitle(testData.indexPattern.name);
await ml.testResources.deleteDataViewByTitle(testData.dataView.name);
}
});

View file

@ -18,7 +18,7 @@ import { MlCustomUrls } from './custom_urls';
export interface DiscoverUrlConfig {
label: string;
indexPattern: string;
indexName: string;
queryEntityFieldNames: string[];
timeRange: TimeRangeType;
timeRangeInterval?: string;
@ -98,7 +98,7 @@ export function MachineLearningDataFrameAnalyticsEditProvider(
);
await mlCommonUI.selectSelectValueByVisibleText(
'mlJobCustomUrlDiscoverIndexPatternInput',
customUrl.indexPattern
customUrl.indexName
);
await customUrls.setCustomUrlQueryEntityFieldNames(customUrl.queryEntityFieldNames);
if (addTimerange) {