From b6fd584c6434d5b0a430852c5bca1f89a1d980f9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 8 Feb 2024 09:22:35 +0000 Subject: [PATCH] [ML] Adds grok highlighting to the file data visualizer (#175913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds grokpattern highlighting to the file data visualizer for semi-structured text files. The first 5 lines of the file are displayed with inline highlighting. Hovering the mouse over displays a tooltip with the field name and type. ![image](https://github.com/elastic/kibana/assets/22172091/7b50aeca-0255-4413-93ef-e44976e798f4) If for whatever reason the highlighting fails, we switch back to the raw text. @szabosteve and @peteharverson I'm not 100% happy with the labels on the tabs, `Highlighted text` and `Raw text`. So suggestions are welcome. Relates to https://github.com/elastic/elasticsearch/pull/104394 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: István Zoltán Szabó --- .../common/types/test_grok_pattern.ts | 13 ++ .../components/file_contents/field_badge.tsx | 62 ++++++++ .../file_contents/file_contents.tsx | 150 +++++++++++++++--- .../file_contents/grok_highlighter.ts | 103 ++++++++++++ .../file_contents/use_text_parser.tsx | 67 ++++++++ .../components/results_view/results_view.tsx | 13 ++ .../plugins/data_visualizer/server/index.ts | 2 +- .../plugins/data_visualizer/server/plugin.ts | 18 ++- .../plugins/data_visualizer/server/routes.ts | 69 ++++++++ x-pack/plugins/data_visualizer/tsconfig.json | 35 ++-- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../data_visualizer/file_data_visualizer.ts | 13 +- .../services/ml/data_visualizer_file_based.ts | 26 +++ 15 files changed, 521 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx create mode 100644 x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx create mode 100644 x-pack/plugins/data_visualizer/server/routes.ts diff --git a/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts b/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts new file mode 100644 index 0000000000000..65ae4a89988de --- /dev/null +++ b/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TestGrokPatternResponse { + matches: Array<{ + matched: boolean; + fields: Record>; + }>; +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx new file mode 100644 index 0000000000000..981b2195c3065 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx @@ -0,0 +1,62 @@ +/* + * 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 React, { FC } from 'react'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { FieldIcon } from '@kbn/react-field'; +import { i18n } from '@kbn/i18n'; +import { getSupportedFieldType } from '../../../common/components/fields_stats_grid/get_field_names'; +import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; + +interface Props { + type: string | undefined; + value: string; + name: string; +} + +export const FieldBadge: FC = ({ type, value, name }) => { + const { euiColorLightestShade, euiColorLightShade } = useCurrentEuiTheme(); + const supportedType = getSupportedFieldType(type ?? 'unknown'); + const tooltip = type + ? i18n.translate('xpack.dataVisualizer.file.fileContents.fieldBadge.tooltip', { + defaultMessage: 'Type: {type}', + values: { type: supportedType }, + }) + : undefined; + return ( + + + + + + + {value} + + + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx index 21c50a7f293b6..789d73888bf35 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx @@ -5,52 +5,150 @@ * 2.0. */ +import React, { FC, useEffect, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { FC } from 'react'; -import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { + EuiTitle, + EuiSpacer, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, +} from '@elastic/eui'; -import { JsonEditor, EDITOR_MODE } from '../json_editor'; +import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import useMountedState from 'react-use/lib/useMountedState'; +import { i18n } from '@kbn/i18n'; +import { EDITOR_MODE, JsonEditor } from '../json_editor'; +import { useGrokHighlighter } from './use_text_parser'; +import { LINE_LIMIT } from './grok_highlighter'; interface Props { data: string; format: string; numberOfLines: number; + semiStructureTextData: SemiStructureTextData | null; } -export const FileContents: FC = ({ data, format, numberOfLines }) => { +interface SemiStructureTextData { + grokPattern?: string; + multilineStartPattern?: string; + excludeLinesPattern?: string; + sampleStart: string; + mappings: FindFileStructureResponse['mappings']; + ecsCompatibility?: string; +} + +function semiStructureTextDataGuard( + semiStructureTextData: SemiStructureTextData | null +): semiStructureTextData is SemiStructureTextData { + return ( + semiStructureTextData !== null && + semiStructureTextData.grokPattern !== undefined && + semiStructureTextData.multilineStartPattern !== undefined + ); +} + +export const FileContents: FC = ({ data, format, numberOfLines, semiStructureTextData }) => { let mode = EDITOR_MODE.TEXT; if (format === EDITOR_MODE.JSON) { mode = EDITOR_MODE.JSON; } + const isMounted = useMountedState(); + const grokHighlighter = useGrokHighlighter(); + + const [isSemiStructureTextData, setIsSemiStructureTextData] = useState( + semiStructureTextDataGuard(semiStructureTextData) + ); + const formattedData = useMemo( + () => limitByNumberOfLines(data, numberOfLines), + [data, numberOfLines] + ); + + const [highlightedLines, setHighlightedLines] = useState(null); + const [showHighlights, setShowHighlights] = useState(isSemiStructureTextData); + + useEffect(() => { + if (isSemiStructureTextData === false) { + return; + } + const { grokPattern, multilineStartPattern, excludeLinesPattern, mappings, ecsCompatibility } = + semiStructureTextData!; - const formattedData = limitByNumberOfLines(data, numberOfLines); + grokHighlighter( + data, + grokPattern!, + mappings, + ecsCompatibility, + multilineStartPattern!, + excludeLinesPattern + ) + .then((docs) => { + if (isMounted()) { + setHighlightedLines(docs); + } + }) + .catch((e) => { + if (isMounted()) { + setHighlightedLines(null); + setIsSemiStructureTextData(false); + } + }); + }, [data, semiStructureTextData, grokHighlighter, isSemiStructureTextData, isMounted]); return ( - - -

- -

-
- -
- -
+ <> + + + +

+ +

+
+
+ {isSemiStructureTextData ? ( + + setShowHighlights(!showHighlights)} + /> + + ) : null} +
+ + + + - -
+ {highlightedLines === null || showHighlights === false ? ( + + ) : ( + <> + {highlightedLines.map((line, i) => ( + <> + {line} + {i === highlightedLines.length - 1 ? null : } + + ))} + + )} + ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts new file mode 100644 index 0000000000000..7be566a5a91b0 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts @@ -0,0 +1,103 @@ +/* + * 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 { MessageImporter } from '@kbn/file-upload-plugin/public'; +import type { HttpSetup } from '@kbn/core/public'; +import type { ImportFactoryOptions } from '@kbn/file-upload-plugin/public/importer'; +import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import type { TestGrokPatternResponse } from '../../../../../common/types/test_grok_pattern'; + +export const LINE_LIMIT = 5; + +type HighlightedLine = Array<{ + word: string; + field?: { + type: string; + name: string; + }; +}>; + +export class GrokHighlighter extends MessageImporter { + constructor(options: ImportFactoryOptions, private http: HttpSetup) { + super(options); + } + + public async createLines( + text: string, + grokPattern: string, + mappings: FindFileStructureResponse['mappings'], + ecsCompatibility: string | undefined + ): Promise { + const docs = this._createDocs(text, false, LINE_LIMIT); + const lines = docs.docs.map((doc) => doc.message); + const matches = await this.testGrokPattern(lines, grokPattern, ecsCompatibility); + + return lines.map((line, index) => { + const { matched, fields } = matches[index]; + if (matched === false) { + return [ + { + word: line, + }, + ]; + } + const sortedFields = Object.entries(fields) + .map(([fieldName, [{ match, offset, length }]]) => { + let type = mappings.properties[fieldName]?.type; + if (type === undefined && fieldName === 'timestamp') { + // it's possible that the timestamp field is not mapped as `timestamp` + // but instead as `@timestamp` + type = mappings.properties['@timestamp']?.type; + } + return { + name: fieldName, + match, + offset, + length, + type, + }; + }) + .sort((a, b) => a.offset - b.offset); + + let offset = 0; + const highlightedLine: HighlightedLine = []; + for (const field of sortedFields) { + highlightedLine.push({ word: line.substring(offset, field.offset) }); + highlightedLine.push({ + word: field.match, + field: { + type: field.type, + name: field.name, + }, + }); + offset = field.offset + field.length; + } + highlightedLine.push({ word: line.substring(offset) }); + return highlightedLine; + }); + } + + private async testGrokPattern( + lines: string[], + grokPattern: string, + ecsCompatibility: string | undefined + ) { + const { matches } = await this.http.fetch( + '/internal/data_visualizer/test_grok_pattern', + { + method: 'POST', + version: '1', + body: JSON.stringify({ + grokPattern, + text: lines, + ecsCompatibility, + }), + } + ); + return matches; + } +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx new file mode 100644 index 0000000000000..183f3ca727d3a --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx @@ -0,0 +1,67 @@ +/* + * 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 React, { useMemo } from 'react'; +import { EuiText } from '@elastic/eui'; +import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import { FieldBadge } from './field_badge'; +import { useDataVisualizerKibana } from '../../../kibana_context'; +import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; +import { GrokHighlighter } from './grok_highlighter'; + +export function useGrokHighlighter() { + const { + services: { http }, + } = useDataVisualizerKibana(); + const { euiSizeL } = useCurrentEuiTheme(); + + const createLines = useMemo( + () => + async ( + text: string, + grokPattern: string, + mappings: FindFileStructureResponse['mappings'], + ecsCompatibility: string | undefined, + multilineStartPattern: string, + excludeLinesPattern: string | undefined + ) => { + const grokHighlighter = new GrokHighlighter( + { multilineStartPattern, excludeLinesPattern }, + http + ); + const lines = await grokHighlighter.createLines( + text, + grokPattern, + mappings, + ecsCompatibility + ); + + return lines.map((line) => { + const formattedWords: JSX.Element[] = []; + for (const { word, field } of line) { + if (field) { + formattedWords.push(); + } else { + formattedWords.push({word}); + } + } + return ( + + {formattedWords} + + ); + }); + }, + [euiSizeL, http] + ); + + return createLines; +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx index 26a727a7a922e..855df5855536a 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import { FILE_FORMATS } from '../../../../../common/constants'; import { FileContents } from '../file_contents'; import { AnalysisSummary } from '../analysis_summary'; import { FieldsStatsGrid } from '../../../common/components/fields_stats_grid'; @@ -48,6 +49,17 @@ export const ResultsView: FC = ({ onCancel, disableImport, }) => { + const semiStructureTextData = + results.format === FILE_FORMATS.SEMI_STRUCTURED_TEXT + ? { + grokPattern: results.grok_pattern, + multilineStartPattern: results.multiline_start_pattern, + sampleStart: results.sample_start, + excludeLinesPattern: results.exclude_lines_pattern, + mappings: results.mappings, + ecsCompatibility: results.ecs_compatibility, + } + : null; return ( @@ -77,6 +89,7 @@ export const ResultsView: FC = ({ data={data} format={results.format} numberOfLines={results.num_lines_analyzed} + semiStructureTextData={semiStructureTextData} /> diff --git a/x-pack/plugins/data_visualizer/server/index.ts b/x-pack/plugins/data_visualizer/server/index.ts index 1f15b498f8777..17db3c1abb603 100644 --- a/x-pack/plugins/data_visualizer/server/index.ts +++ b/x-pack/plugins/data_visualizer/server/index.ts @@ -9,5 +9,5 @@ import { PluginInitializerContext } from '@kbn/core/server'; export const plugin = async (initializerContext: PluginInitializerContext) => { const { DataVisualizerPlugin } = await import('./plugin'); - return new DataVisualizerPlugin(); + return new DataVisualizerPlugin(initializerContext); }; diff --git a/x-pack/plugins/data_visualizer/server/plugin.ts b/x-pack/plugins/data_visualizer/server/plugin.ts index 5f16df8b5ffb7..0e70f756f9b21 100644 --- a/x-pack/plugins/data_visualizer/server/plugin.ts +++ b/x-pack/plugins/data_visualizer/server/plugin.ts @@ -5,18 +5,30 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; -import { StartDeps, SetupDeps } from './types'; +import type { + CoreSetup, + CoreStart, + Plugin, + Logger, + PluginInitializerContext, +} from '@kbn/core/server'; +import type { StartDeps, SetupDeps } from './types'; import { registerWithCustomIntegrations } from './register_custom_integration'; +import { routes } from './routes'; export class DataVisualizerPlugin implements Plugin { - constructor() {} + private readonly _logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this._logger = initializerContext.logger.get(); + } setup(coreSetup: CoreSetup, plugins: SetupDeps) { // home-plugin required if (plugins.home && plugins.customIntegrations) { registerWithCustomIntegrations(plugins.customIntegrations); } + routes(coreSetup, this._logger); } start(core: CoreStart) {} diff --git a/x-pack/plugins/data_visualizer/server/routes.ts b/x-pack/plugins/data_visualizer/server/routes.ts new file mode 100644 index 0000000000000..c4e286f9671d1 --- /dev/null +++ b/x-pack/plugins/data_visualizer/server/routes.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, Logger } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import type { StartDeps } from './types'; +import { wrapError } from './utils/error_wrapper'; +import type { TestGrokPatternResponse } from '../common/types/test_grok_pattern'; + +/** + * @apiGroup DataVisualizer + * + * @api {post} /internal/data_visualizer/test_grok_pattern Tests a grok pattern against a sample of text + * @apiName testGrokPattern + * @apiDescription Tests a grok pattern against a sample of text and return the positions of the fields + */ +export function routes(coreSetup: CoreSetup, logger: Logger) { + const router = coreSetup.http.createRouter(); + + router.versioned + .post({ + path: '/internal/data_visualizer/test_grok_pattern', + access: 'internal', + options: { + tags: ['access:fileUpload:analyzeFile'], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: schema.object({ + grokPattern: schema.string(), + text: schema.arrayOf(schema.string()), + ecsCompatibility: schema.maybe(schema.string()), + }), + }, + }, + }, + async (context, request, response) => { + try { + const esClient = (await context.core).elasticsearch.client; + const body = await esClient.asInternalUser.transport.request({ + method: 'GET', + path: `/_text_structure/test_grok_pattern`, + body: { + grok_pattern: request.body.grokPattern, + text: request.body.text, + }, + ...(request.body.ecsCompatibility + ? { + querystring: { ecs_compatibility: request.body.ecsCompatibility }, + } + : {}), + }); + + return response.ok({ body }); + } catch (e) { + logger.warn(`Unable to test grok pattern ${e.message}`); + return response.customError(wrapError(e)); + } + } + ); +} diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index af962ac08b6e8..aad960d856da3 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -17,20 +17,28 @@ "@kbn/aiops-utils", "@kbn/charts-plugin", "@kbn/cloud-plugin", + "@kbn/code-editor", + "@kbn/config-schema", "@kbn/core-execution-context-common", + "@kbn/core-notifications-browser", "@kbn/core", "@kbn/custom-integrations-plugin", "@kbn/data-plugin", + "@kbn/data-service", "@kbn/data-view-field-editor-plugin", "@kbn/data-views-plugin", "@kbn/datemath", "@kbn/discover-plugin", + "@kbn/ebt-tools", "@kbn/embeddable-plugin", "@kbn/embeddable-plugin", "@kbn/es-query", + "@kbn/es-types", "@kbn/es-ui-shared-plugin", + "@kbn/esql-utils", "@kbn/field-formats-plugin", "@kbn/field-types", + "@kbn/field-utils", "@kbn/file-upload-plugin", "@kbn/home-plugin", "@kbn/i18n-react", @@ -41,41 +49,34 @@ "@kbn/maps-plugin", "@kbn/ml-agg-utils", "@kbn/ml-cancellable-search", + "@kbn/ml-chi2test", + "@kbn/ml-data-grid", "@kbn/ml-date-picker", + "@kbn/ml-error-utils", + "@kbn/ml-in-memory-table", "@kbn/ml-is-defined", "@kbn/ml-is-populated-object", + "@kbn/ml-kibana-theme", "@kbn/ml-local-storage", "@kbn/ml-nested-property", "@kbn/ml-number-utils", "@kbn/ml-query-utils", + "@kbn/ml-random-sampler-utils", + "@kbn/ml-string-hash", "@kbn/ml-url-state", - "@kbn/ml-data-grid", - "@kbn/ml-error-utils", - "@kbn/ml-kibana-theme", - "@kbn/ml-in-memory-table", "@kbn/react-field", "@kbn/rison", "@kbn/saved-search-plugin", "@kbn/security-plugin", "@kbn/share-plugin", "@kbn/test-jest-helpers", + "@kbn/text-based-languages", "@kbn/ui-actions-plugin", + "@kbn/ui-theme", "@kbn/unified-search-plugin", "@kbn/usage-collection-plugin", "@kbn/utility-types", - "@kbn/ml-string-hash", - "@kbn/ml-random-sampler-utils", - "@kbn/data-service", - "@kbn/core-notifications-browser", - "@kbn/ebt-tools", - "@kbn/ml-chi2test", - "@kbn/field-utils", - "@kbn/visualization-utils", - "@kbn/text-based-languages", - "@kbn/code-editor", - "@kbn/es-types", - "@kbn/ui-theme", - "@kbn/esql-utils" + "@kbn/visualization-utils" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c812c47f9aa48..b8d9c9eb44c43 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12174,7 +12174,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{length, plural, one { {lg} } many { Le groupe de lettres {lg} } other { Le groupe de lettres {lg} }} en {format} n'est pas compatible, car il n'est pas précédé de ss ni d'un séparateur de {sep}", "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{length, plural, one { {lg} } many { Le groupe de lettres {lg} } other { Le groupe de lettres {lg} }} en {format} n'est pas compatible", "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "Le format d'horodatage {timestampFormat} n'est pas compatible, car il contient un point d'interrogation ({fieldPlaceholder})", - "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "{numberOfLines, plural, one {# ligne} many {# lignes} other {# lignes}} premier(s)", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "La taille du fichier que vous avez sélectionné pour le chargement dépasse la taille maximale autorisée de {maxFileSizeFormatted} de {diffFormatted}", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "La taille du fichier que vous avez sélectionné pour le chargement est de {fileSizeFormatted}, ce qui dépasse la taille maximale autorisée de {maxFileSizeFormatted}", "xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "Impossible d'importer {importFailuresLength} document(s) sur {docCount}. Cela peut être dû au manque de correspondance entre les lignes et le modèle Grok.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 10bbca87a9cad..cf3ea4c3fe0a9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12187,7 +12187,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{format}の文字{length, plural, other { グループ{lg} }}は、前にssと{sep}の区切り文字が付いていないため、サポートされません", "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{format}の文字{length, plural, other { グループ{lg} }}はサポートされていません", "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "タイムスタンプフォーマット {timestampFormat} は、疑問符({fieldPlaceholder})が含まれているためサポートされていません", - "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "最初の{numberOfLines, plural, other {#行}}件", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "アップロードするよう選択されたファイルのサイズが {diffFormatted} に許可された最大サイズの {maxFileSizeFormatted} を超えています", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "アップロードするよう選択されたファイルのサイズは {fileSizeFormatted} で、許可された最大サイズの {maxFileSizeFormatted} を超えています", "xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "{docCount}件中{importFailuresLength}件のドキュメントをインポートできません。行が Grok パターンと一致していないことが原因の可能性があります。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bd37c0d9f3fb6..26029615545f8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12281,7 +12281,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{format}的字母 {length, plural, other { 组 {lg} }} 不受支持,因为其未前置 ss 和 {sep} 中的分隔符", "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{format}的字母 {length, plural, other { 组 {lg} }} 不受支持", "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "时间戳格式 {timestampFormat} 不受支持,因为其包含问号字符 ({fieldPlaceholder})", - "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "前 {numberOfLines, plural, other {# 行}}", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "您选择用于上传的文件大小超过上限值 {maxFileSizeFormatted} 的 {diffFormatted}", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "您选择用于上传的文件大小为 {fileSizeFormatted},超过上限值 {maxFileSizeFormatted}", "xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "无法导入 {importFailuresLength} 个文档(共 {docCount} 个)。这可能是由于行与 Grok 模式不匹配。", diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index 24437e02e6907..3a859249fe234 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { results: { title: 'artificial_server_log', - numberOfFields: 4, + highlightedText: true, }, metricFields: [ { @@ -104,7 +104,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { results: { title: 'geo_file.csv', - numberOfFields: 3, + highlightedText: false, }, metricFields: [], nonMetricFields: [ @@ -146,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { results: { title: 'missing_end_of_file_newline.csv', - numberOfFields: 3, + highlightedText: false, }, metricFields: [ { @@ -217,6 +217,13 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the components of the file details page'); await ml.dataVisualizerFileBased.assertFileTitle(testData.expected.results.title); await ml.dataVisualizerFileBased.assertFileContentPanelExists(); + await ml.dataVisualizerFileBased.assertFileContentHighlightingSwitchExists( + testData.expected.results.highlightedText + ); + await ml.dataVisualizerFileBased.assertFileContentHighlighting( + testData.expected.results.highlightedText, + testData.expected.totalFieldsCount - 1 // -1 for the message field + ); await ml.dataVisualizerFileBased.assertSummaryPanelExists(); await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index 0b8effd17cbb0..df95eddd957f8 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -49,6 +49,32 @@ export function MachineLearningDataVisualizerFileBasedProvider( await testSubjects.existOrFail('dataVisualizerFileFileContentPanel'); }, + async assertFileContentHighlightingSwitchExists(exist: boolean) { + const tabs = await testSubjects.findAll('dataVisualizerFileContentsHighlightingSwitch'); + expect(tabs.length).to.eql( + exist ? 1 : 0, + `Expected file content highlighting switch to ${exist ? 'exist' : 'not exist'}, but found ${ + tabs.length + }` + ); + }, + + async assertFileContentHighlighting(highlighted: boolean, numberOfFields: number) { + const lines = await testSubjects.findAll('dataVisualizerHighlightedLine', 1000); + const linesExist = lines.length > 0; + expect(linesExist).to.eql( + highlighted, + `Expected file content highlighting to be '${highlighted ? 'enabled' : 'disabled'}'` + ); + const expectedNumberOfFields = highlighted ? numberOfFields : 0; + const foundFields = (await lines[0]?.findAllByTestSubject('dataVisualizerFieldBadge')) ?? []; + + expect(foundFields.length).to.eql( + expectedNumberOfFields, + `Expected ${expectedNumberOfFields} fields to be highlighted, but found ${foundFields.length}` + ); + }, + async assertSummaryPanelExists() { await testSubjects.existOrFail('dataVisualizerFileSummaryPanel'); },