mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Merge branch 'main' into ui-actions-refactor
This commit is contained in:
commit
bccf61bd4d
75 changed files with 1441 additions and 447 deletions
|
@ -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
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -10,7 +10,7 @@ export {
|
|||
getDataGridSchemaFromESFieldType,
|
||||
getDataGridSchemaFromKibanaFieldType,
|
||||
getFeatureImportance,
|
||||
getFieldsFromKibanaIndexPattern,
|
||||
getFieldsFromKibanaDataView,
|
||||
getNestedOrEscapedVal,
|
||||
getProcessedFields,
|
||||
getTopClasses,
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -158,9 +158,9 @@ export interface UseIndexDataReturnType
|
|||
*/
|
||||
renderCellValue: RenderCellValue;
|
||||
/**
|
||||
* Optional index pattern fields.
|
||||
* Optional data view fields.
|
||||
*/
|
||||
indexPatternFields?: string[];
|
||||
dataViewFields?: string[];
|
||||
/**
|
||||
* Optional time range.
|
||||
*/
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ function createBaseOptions({
|
|||
};
|
||||
}
|
||||
|
||||
describe('getHosts', () => {
|
||||
describe('getContainers', () => {
|
||||
let getApmIndicesMock = createGetApmIndicesMock();
|
||||
let metricsDataClientMock = MetricsDataClientMock.create();
|
||||
let baseOptions = createBaseOptions({ getApmIndicesMock, metricsDataClientMock });
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
70
x-pack/plugins/asset_manager/server/routes/assets/pods.ts
Normal file
70
x-pack/plugins/asset_manager/server/routes/assets/pods.ts
Normal 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}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 },
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { IndexPatternPrompt } from './index_pattern_prompt';
|
||||
export { DataViewPrompt } from './data_view_prompt';
|
|
@ -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) && (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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'] }
|
||||
|
|
|
@ -39,7 +39,7 @@ const getMockState = ({
|
|||
jobIdEmpty: false,
|
||||
jobIdValid: true,
|
||||
jobIdExists: false,
|
||||
createIndexPattern: false,
|
||||
createDataView: false,
|
||||
},
|
||||
jobConfig: {
|
||||
source: { index },
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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(':');
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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()) })
|
||||
);
|
|
@ -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]);
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": "この深さを超える決定木は、損失計算でペナルティがあります。",
|
||||
|
|
|
@ -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": "超过此深度的决策树将在损失计算中被罚分。",
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
100
x-pack/test/api_integration/apis/asset_manager/tests/pods.ts
Normal file
100
x-pack/test/api_integration/apis/asset_manager/tests/pods.ts
Normal 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)));
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
|
||||
const testDiscoverCustomUrl: DiscoverUrlConfig = {
|
||||
label: 'Show data',
|
||||
indexPattern: 'ft_farequote',
|
||||
indexName: 'ft_farequote',
|
||||
queryEntityFieldNames: ['airline'],
|
||||
timeRange: TIME_RANGE_TYPE.AUTO,
|
||||
};
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue