mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
ES|QL pattern formatting (#222871)
Adds a recommended query for the `CATEGORIZE` function in ES|QL. Adds keyword highlighting for the patterns and the ability to open a new Discover tab to filter for docs which match the selected pattern. https://github.com/user-attachments/assets/9ed8c5b0-7e92-4cc8-88dd-cb7749b5ffd3 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
831004deac
commit
0d2930b3d0
49 changed files with 1112 additions and 61 deletions
12
.github/CODEOWNERS
vendored
12
.github/CODEOWNERS
vendored
|
@ -410,6 +410,7 @@ src/platform/packages/shared/home/sample_data_card @elastic/appex-sharedux
|
||||||
src/platform/packages/shared/home/sample_data_tab @elastic/appex-sharedux
|
src/platform/packages/shared/home/sample_data_tab @elastic/appex-sharedux
|
||||||
src/platform/packages/shared/home/sample_data_types @elastic/appex-sharedux
|
src/platform/packages/shared/home/sample_data_types @elastic/appex-sharedux
|
||||||
src/platform/packages/shared/kbn-actions-types @elastic/response-ops
|
src/platform/packages/shared/kbn-actions-types @elastic/response-ops
|
||||||
|
src/platform/packages/shared/kbn-aiops-utils @elastic/ml-ui
|
||||||
src/platform/packages/shared/kbn-alerting-types @elastic/response-ops
|
src/platform/packages/shared/kbn-alerting-types @elastic/response-ops
|
||||||
src/platform/packages/shared/kbn-alerts-as-data-utils @elastic/response-ops
|
src/platform/packages/shared/kbn-alerts-as-data-utils @elastic/response-ops
|
||||||
src/platform/packages/shared/kbn-alerts-ui-shared @elastic/response-ops
|
src/platform/packages/shared/kbn-alerts-ui-shared @elastic/response-ops
|
||||||
|
@ -1220,6 +1221,7 @@ x-pack/test_serverless/api_integration/test_suites/common/platform_security @ela
|
||||||
/x-pack/test_serverless/functional/test_suites/common/examples/search_examples @elastic/kibana-data-discovery
|
/x-pack/test_serverless/functional/test_suites/common/examples/search_examples @elastic/kibana-data-discovery
|
||||||
/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples @elastic/kibana-data-discovery
|
/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples @elastic/kibana-data-discovery
|
||||||
/x-pack/test_serverless/functional/test_suites/common/management/data_views @elastic/kibana-data-discovery
|
/x-pack/test_serverless/functional/test_suites/common/management/data_views @elastic/kibana-data-discovery
|
||||||
|
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/patterns @elastic/ml-ui
|
||||||
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations
|
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations
|
||||||
# TODO: this deprecation_logs folder should be owned by kibana management team after 9.0
|
# TODO: this deprecation_logs folder should be owned by kibana management team after 9.0
|
||||||
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/deprecation_logs @elastic/kibana-data-discovery @elastic/kibana-core
|
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/deprecation_logs @elastic/kibana-data-discovery @elastic/kibana-core
|
||||||
|
@ -2836,11 +2838,11 @@ src/platform/testfunctional/page_objects/solution_navigation.ts @elastic/appex-s
|
||||||
/x-pack/test/api_integration/deployment_agnostic/apis/intercepts/*.ts @elastic/appex-sharedux
|
/x-pack/test/api_integration/deployment_agnostic/apis/intercepts/*.ts @elastic/appex-sharedux
|
||||||
|
|
||||||
# OpenAPI spec files
|
# OpenAPI spec files
|
||||||
oas_docs/linters @elastic/core-docs @elastic/experience-docs
|
oas_docs/linters @elastic/core-docs @elastic/experience-docs
|
||||||
oas_docs/overlays @elastic/core-docs @elastic/experience-docs
|
oas_docs/overlays @elastic/core-docs @elastic/experience-docs
|
||||||
oas_docs/kibana.info.serverless.yaml @elastic/core-docs @elastic/experience-docs
|
oas_docs/kibana.info.serverless.yaml @elastic/core-docs @elastic/experience-docs
|
||||||
oas_docs/kibana.info.yaml @elastic/core-docs @elastic/experience-docs
|
oas_docs/kibana.info.yaml @elastic/core-docs @elastic/experience-docs
|
||||||
oas_docs/output @elastic/core-docs @elastic/experience-docs
|
oas_docs/output @elastic/core-docs @elastic/experience-docs
|
||||||
|
|
||||||
# Documentation settings files
|
# Documentation settings files
|
||||||
docs/settings-gen @elastic/platform-docs @elastic/experience-docs
|
docs/settings-gen @elastic/platform-docs @elastic/experience-docs
|
||||||
|
|
|
@ -183,6 +183,7 @@
|
||||||
"@kbn/aiops-log-rate-analysis": "link:x-pack/platform/packages/shared/ml/aiops_log_rate_analysis",
|
"@kbn/aiops-log-rate-analysis": "link:x-pack/platform/packages/shared/ml/aiops_log_rate_analysis",
|
||||||
"@kbn/aiops-plugin": "link:x-pack/platform/plugins/shared/aiops",
|
"@kbn/aiops-plugin": "link:x-pack/platform/plugins/shared/aiops",
|
||||||
"@kbn/aiops-test-utils": "link:x-pack/platform/packages/private/ml/aiops_test_utils",
|
"@kbn/aiops-test-utils": "link:x-pack/platform/packages/private/ml/aiops_test_utils",
|
||||||
|
"@kbn/aiops-utils": "link:src/platform/packages/shared/kbn-aiops-utils",
|
||||||
"@kbn/alerting-api-integration-test-plugin": "link:x-pack/platform/test/alerting_api_integration/common/plugins/alerts",
|
"@kbn/alerting-api-integration-test-plugin": "link:x-pack/platform/test/alerting_api_integration/common/plugins/alerts",
|
||||||
"@kbn/alerting-comparators": "link:x-pack/platform/packages/shared/kbn-alerting-comparators",
|
"@kbn/alerting-comparators": "link:x-pack/platform/packages/shared/kbn-alerting-comparators",
|
||||||
"@kbn/alerting-example-plugin": "link:x-pack/examples/alerting_example",
|
"@kbn/alerting-example-plugin": "link:x-pack/examples/alerting_example",
|
||||||
|
|
3
src/platform/packages/shared/kbn-aiops-utils/README.md
Normal file
3
src/platform/packages/shared/kbn-aiops-utils/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# @kbn/aiops-utils
|
||||||
|
|
||||||
|
AIOps utils for plugins which are not in x-pack
|
13
src/platform/packages/shared/kbn-aiops-utils/index.ts
Normal file
13
src/platform/packages/shared/kbn-aiops-utils/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
getCategorizationDataViewField,
|
||||||
|
getCategorizationField,
|
||||||
|
} from './src/get_categorization_field';
|
14
src/platform/packages/shared/kbn-aiops-utils/jest.config.js
Normal file
14
src/platform/packages/shared/kbn-aiops-utils/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
preset: '@kbn/test',
|
||||||
|
rootDir: '../../../../..',
|
||||||
|
roots: ['<rootDir>/src/platform/packages/shared/kbn-aiops-utils'],
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "shared-common",
|
||||||
|
"id": "@kbn/aiops-utils",
|
||||||
|
"owner": "@elastic/ml-ui",
|
||||||
|
"group": "platform",
|
||||||
|
"visibility": "shared"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@kbn/aiops-utils",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createStubDataView } from '@kbn/data-views-plugin/common/stubs';
|
||||||
|
import { getCategorizationField } from './get_categorization_field';
|
||||||
|
import { getCategorizationDataViewField } from './get_categorization_field';
|
||||||
|
|
||||||
|
describe('get_categorization_field utils', () => {
|
||||||
|
describe('getCategorizationField', () => {
|
||||||
|
it('returns "message" if present', () => {
|
||||||
|
const fields = ['foo', 'bar', 'message', 'baz'];
|
||||||
|
expect(getCategorizationField(fields)).toBe('message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "error.message" if present and "message" is not', () => {
|
||||||
|
const fields = ['foo', 'error.message', 'baz'];
|
||||||
|
expect(getCategorizationField(fields)).toBe('error.message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "event.original" if present and others are not', () => {
|
||||||
|
const fields = ['event.original', 'foo', 'bar'];
|
||||||
|
expect(getCategorizationField(fields)).toBe('event.original');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns first field if none of the priority fields are present', () => {
|
||||||
|
const fields = ['foo', 'bar', 'baz'];
|
||||||
|
expect(getCategorizationField(fields)).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns first field, skipping meta data fields, if none of the priority fields are present', () => {
|
||||||
|
const fields = ['_id', 'foo', 'bar', 'baz'];
|
||||||
|
expect(getCategorizationField(fields)).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the first matching field according to priority', () => {
|
||||||
|
const fields = ['event.original', 'error.message', 'message'];
|
||||||
|
expect(getCategorizationField(fields)).toBe('message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty array', () => {
|
||||||
|
expect(getCategorizationField([])).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCategorizationDataViewField', () => {
|
||||||
|
it('returns messageField as "message" if present', () => {
|
||||||
|
const dataView = createStubDataView({
|
||||||
|
spec: {
|
||||||
|
id: 'test',
|
||||||
|
fields: {
|
||||||
|
foo: {
|
||||||
|
searchable: false,
|
||||||
|
aggregatable: false,
|
||||||
|
name: 'foo',
|
||||||
|
type: 'type',
|
||||||
|
esTypes: ['keyword'],
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'message',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
'error.message': {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'error.message',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = getCategorizationDataViewField(dataView);
|
||||||
|
expect(result.messageField?.name).toBe('message');
|
||||||
|
expect(result.dataViewFields.map((f) => f.name)).toContain('message');
|
||||||
|
expect(result.dataViewFields.length).toBe(2);
|
||||||
|
});
|
||||||
|
it('returns messageField as "error.message" if "message" is not present', () => {
|
||||||
|
const dataView = createStubDataView({
|
||||||
|
spec: {
|
||||||
|
id: 'test2',
|
||||||
|
fields: {
|
||||||
|
foo: {
|
||||||
|
searchable: false,
|
||||||
|
aggregatable: false,
|
||||||
|
name: 'foo',
|
||||||
|
type: 'type',
|
||||||
|
esTypes: ['keyword'],
|
||||||
|
},
|
||||||
|
'error.message': {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'error.message',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'bar',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = getCategorizationDataViewField(dataView);
|
||||||
|
expect(result.messageField?.name).toBe('error.message');
|
||||||
|
expect(result.dataViewFields.map((f) => f.name)).toContain('error.message');
|
||||||
|
expect(result.dataViewFields.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty fields array', () => {
|
||||||
|
const dataView = createStubDataView({
|
||||||
|
spec: {
|
||||||
|
id: 'test6',
|
||||||
|
fields: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = getCategorizationDataViewField(dataView);
|
||||||
|
expect(result.messageField).toBeNull();
|
||||||
|
expect(result.dataViewFields.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the first matching field according to priority', () => {
|
||||||
|
const dataView = createStubDataView({
|
||||||
|
spec: {
|
||||||
|
id: 'test7',
|
||||||
|
fields: {
|
||||||
|
'event.original': {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'event.original',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
'error.message': {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'error.message',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'message',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = getCategorizationDataViewField(dataView);
|
||||||
|
expect(result.messageField?.name).toBe('message');
|
||||||
|
expect(result.dataViewFields.map((f) => f.name)).toEqual(
|
||||||
|
expect.arrayContaining(['message', 'error.message', 'event.original'])
|
||||||
|
);
|
||||||
|
expect(result.dataViewFields.length).toBe(3);
|
||||||
|
});
|
||||||
|
it('returns the first field if no priority fields are present', () => {
|
||||||
|
const dataView = createStubDataView({
|
||||||
|
spec: {
|
||||||
|
id: 'test8',
|
||||||
|
fields: {
|
||||||
|
foo: {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'foo',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'bar',
|
||||||
|
type: 'text',
|
||||||
|
esTypes: ['text'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = getCategorizationDataViewField(dataView);
|
||||||
|
expect(result.messageField?.name).toBe('foo');
|
||||||
|
expect(result.dataViewFields.map((f) => f.name)).toEqual(
|
||||||
|
expect.arrayContaining(['foo', 'bar'])
|
||||||
|
);
|
||||||
|
expect(result.dataViewFields.length).toBe(2);
|
||||||
|
});
|
||||||
|
it('returns null messageField if no text fields are present', () => {
|
||||||
|
const dataView = createStubDataView({
|
||||||
|
spec: {
|
||||||
|
id: 'test9',
|
||||||
|
fields: {
|
||||||
|
foo: {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'foo',
|
||||||
|
type: 'keyword',
|
||||||
|
esTypes: ['keyword'],
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
searchable: true,
|
||||||
|
aggregatable: true,
|
||||||
|
name: 'bar',
|
||||||
|
type: 'keyword',
|
||||||
|
esTypes: ['keyword'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = getCategorizationDataViewField(dataView);
|
||||||
|
expect(result.messageField).toBeNull();
|
||||||
|
expect(result.dataViewFields.map((f) => f.name)).toEqual(expect.arrayContaining([]));
|
||||||
|
expect(result.dataViewFields.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||||
|
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||||
|
|
||||||
|
const FIELD_PRIORITY = ['message', 'error.message', 'event.original'];
|
||||||
|
|
||||||
|
const METADATA_FIELDS = [
|
||||||
|
'_version',
|
||||||
|
'_id',
|
||||||
|
'_index',
|
||||||
|
'_source',
|
||||||
|
'_ignored',
|
||||||
|
'_index_mode',
|
||||||
|
'_score',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns the categorization field from the list of fields.
|
||||||
|
* It checks for the presence of 'message', 'error.message', or 'event.original' in that order.
|
||||||
|
* If none of these fields are present, it returns the first field from the list,
|
||||||
|
* Assumes text fields have been passed in the `fields` array.
|
||||||
|
*
|
||||||
|
* @param fields, the list of fields to check
|
||||||
|
* @returns string | undefined, the categorization field if found, otherwise undefined
|
||||||
|
*/
|
||||||
|
export function getCategorizationField(fields: string[]): string | undefined {
|
||||||
|
const fieldSet = new Set(fields);
|
||||||
|
for (const field of FIELD_PRIORITY) {
|
||||||
|
if (fieldSet.has(field)) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out metadata fields
|
||||||
|
const filteredFields = fields.filter((field) => !METADATA_FIELDS.includes(field));
|
||||||
|
return filteredFields[0] ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns the categorization field from the DataView.
|
||||||
|
* It checks for the presence of 'message', 'error.message', or 'event.original' in that order.
|
||||||
|
* If none of these fields are present, it returns the first text field from the DataView.
|
||||||
|
*
|
||||||
|
* @param dataView, the DataView to check
|
||||||
|
* @returns an object containing the message field DataViewField and dataViewFields
|
||||||
|
*/
|
||||||
|
export function getCategorizationDataViewField(dataView: DataView): {
|
||||||
|
messageField: DataViewField | null;
|
||||||
|
dataViewFields: DataViewField[];
|
||||||
|
} {
|
||||||
|
const dataViewFields = dataView.fields.filter((f) => f.esTypes?.includes(ES_FIELD_TYPES.TEXT));
|
||||||
|
const categorizationFieldName = getCategorizationField(dataViewFields.map((f) => f.name));
|
||||||
|
if (categorizationFieldName) {
|
||||||
|
return {
|
||||||
|
messageField: dataView.fields.getByName(categorizationFieldName) ?? null,
|
||||||
|
dataViewFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageField: null,
|
||||||
|
dataViewFields,
|
||||||
|
};
|
||||||
|
}
|
22
src/platform/packages/shared/kbn-aiops-utils/tsconfig.json
Normal file
22
src/platform/packages/shared/kbn-aiops-utils/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types",
|
||||||
|
"types": [
|
||||||
|
"jest",
|
||||||
|
"node",
|
||||||
|
"react"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/data-views-plugin",
|
||||||
|
"@kbn/field-types",
|
||||||
|
]
|
||||||
|
}
|
|
@ -44,6 +44,7 @@ export {
|
||||||
getCategorizeColumns,
|
getCategorizeColumns,
|
||||||
extractCategorizeTokens,
|
extractCategorizeTokens,
|
||||||
getArgsFromRenameFunction,
|
getArgsFromRenameFunction,
|
||||||
|
getCategorizeField,
|
||||||
} from './src';
|
} from './src';
|
||||||
|
|
||||||
export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';
|
export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';
|
||||||
|
|
|
@ -26,6 +26,7 @@ export {
|
||||||
fixESQLQueryWithVariables,
|
fixESQLQueryWithVariables,
|
||||||
getCategorizeColumns,
|
getCategorizeColumns,
|
||||||
getArgsFromRenameFunction,
|
getArgsFromRenameFunction,
|
||||||
|
getCategorizeField,
|
||||||
} from './utils/query_parsing_helpers';
|
} from './utils/query_parsing_helpers';
|
||||||
export { queryCannotBeSampled } from './utils/query_cannot_be_sampled';
|
export { queryCannotBeSampled } from './utils/query_cannot_be_sampled';
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -48,5 +48,6 @@ describe('extractCategorizeTokens()', () => {
|
||||||
expect(extractCategorizeTokens(regexString4)).toStrictEqual(['someString']);
|
expect(extractCategorizeTokens(regexString4)).toStrictEqual(['someString']);
|
||||||
const regexString5 = 'justAString'; // No '.*?' or '.+?'
|
const regexString5 = 'justAString'; // No '.*?' or '.+?'
|
||||||
expect(extractCategorizeTokens(regexString5)).toStrictEqual(['justAString']);
|
expect(extractCategorizeTokens(regexString5)).toStrictEqual(['justAString']);
|
||||||
|
expect(extractCategorizeTokens('.*?foo\\.bar.*?')).toEqual(['foo.bar']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
export function extractCategorizeTokens(regexString: string) {
|
export function extractCategorizeTokens(regexString: string) {
|
||||||
let cleanedString = regexString;
|
let cleanedString = regexString;
|
||||||
|
|
||||||
|
// Remove backslashes
|
||||||
|
cleanedString = cleanedString.replace(/\\/g, '');
|
||||||
|
|
||||||
// Strip leading '.*?'
|
// Strip leading '.*?'
|
||||||
if (cleanedString.startsWith('.*?')) {
|
if (cleanedString.startsWith('.*?')) {
|
||||||
cleanedString = cleanedString.substring('.*?'.length);
|
cleanedString = cleanedString.substring('.*?'.length);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
fixESQLQueryWithVariables,
|
fixESQLQueryWithVariables,
|
||||||
getCategorizeColumns,
|
getCategorizeColumns,
|
||||||
getArgsFromRenameFunction,
|
getArgsFromRenameFunction,
|
||||||
|
getCategorizeField,
|
||||||
} from './query_parsing_helpers';
|
} from './query_parsing_helpers';
|
||||||
import { monaco } from '@kbn/monaco';
|
import { monaco } from '@kbn/monaco';
|
||||||
import { parse, walk } from '@kbn/esql-ast';
|
import { parse, walk } from '@kbn/esql-ast';
|
||||||
|
@ -815,4 +816,43 @@ describe('esql query helpers', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getCategorizeField', () => {
|
||||||
|
it('should return the field used in categorize', () => {
|
||||||
|
const esql = 'FROM index | STATS COUNT() BY categorize(field1)';
|
||||||
|
const expected = ['field1'];
|
||||||
|
expect(getCategorizeField(esql)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the field used in categorize for multiple breakdowns', () => {
|
||||||
|
const esql = 'FROM index | STATS COUNT() BY categorize(field1), field2';
|
||||||
|
const expected = ['field1'];
|
||||||
|
expect(getCategorizeField(esql)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the field used in categorize for multiple breakdowns with BUCKET', () => {
|
||||||
|
const esql =
|
||||||
|
'FROM index | STATS count_per_day = COUNT() BY Pattern=CATEGORIZE(message), @timestamp=BUCKET(@timestamp, 1 day)';
|
||||||
|
const expected = ['message'];
|
||||||
|
expect(getCategorizeField(esql)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the field used in categorize if the result is stored in a new column', () => {
|
||||||
|
const esql = 'FROM index | STATS COUNT() BY pattern = categorize(field1)';
|
||||||
|
const expected = ['field1'];
|
||||||
|
expect(getCategorizeField(esql)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the field used in categorize for a complex query', () => {
|
||||||
|
const esql =
|
||||||
|
'FROM index | STATS count_per_day = COUNT() BY Pattern=CATEGORIZE(message), @timestamp=BUCKET(@timestamp, 1 day) | STATS Count=SUM(count_per_day), Trend=VALUES(count_per_day) BY Pattern';
|
||||||
|
const expected = ['message'];
|
||||||
|
expect(getCategorizeField(esql)).toEqual(expected);
|
||||||
|
});
|
||||||
|
it('should return an empty array if no categorize is present', () => {
|
||||||
|
const esql = 'FROM index | STATS COUNT() BY field1';
|
||||||
|
const expected: string[] = [];
|
||||||
|
expect(getCategorizeField(esql)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -362,3 +362,55 @@ export const getArgsFromRenameFunction = (
|
||||||
renamed: renameFunction.args[0] as ESQLColumn,
|
renamed: renameFunction.args[0] as ESQLColumn,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the fields used in the CATEGORIZE function from an ESQL query.
|
||||||
|
* @param esql: string - The ESQL query string
|
||||||
|
*/
|
||||||
|
export const getCategorizeField = (esql: string): string[] => {
|
||||||
|
const { root } = parse(esql);
|
||||||
|
const statsCommand = root.commands.find(({ name }) => name === 'stats');
|
||||||
|
if (!statsCommand) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const options: ESQLCommandOption[] = [];
|
||||||
|
const columns: string[] = [];
|
||||||
|
|
||||||
|
walk(statsCommand, {
|
||||||
|
visitCommandOption: (node) => options.push(node),
|
||||||
|
});
|
||||||
|
|
||||||
|
const statsByOptions = options.find(({ name }) => name === 'by');
|
||||||
|
|
||||||
|
// categorize is part of the stats by command
|
||||||
|
if (!statsByOptions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const categorizeOptions = statsByOptions.args.filter((arg) => {
|
||||||
|
return (arg as ESQLFunction).text.toLowerCase().indexOf('categorize') !== -1;
|
||||||
|
}) as ESQLFunction[];
|
||||||
|
|
||||||
|
if (categorizeOptions.length) {
|
||||||
|
categorizeOptions.forEach((arg) => {
|
||||||
|
// ... STATS ... BY CATEGORIZE(field)
|
||||||
|
if (isFunctionExpression(arg) && arg.name === 'categorize') {
|
||||||
|
walk(arg, {
|
||||||
|
visitColumn: (node) => columns.push(node.name),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ... STATS ... BY pattern = CATEGORIZE(field)
|
||||||
|
walk(arg, {
|
||||||
|
visitFunction: (node) => {
|
||||||
|
if (node.name === 'categorize') {
|
||||||
|
const columnArgs = node.args.filter((a) => isColumn(a));
|
||||||
|
columnArgs.forEach((c) => columns.push((c as ESQLColumn).name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
|
@ -110,6 +110,7 @@ describe('autocomplete.suggest', () => {
|
||||||
const recommendedQueries = getRecommendedQueries({
|
const recommendedQueries = getRecommendedQueries({
|
||||||
fromCommand: '',
|
fromCommand: '',
|
||||||
timeField: 'dateField',
|
timeField: 'dateField',
|
||||||
|
categorizationField: 'textField',
|
||||||
});
|
});
|
||||||
const { assertSuggestions } = await setup();
|
const { assertSuggestions } = await setup();
|
||||||
const expected = [
|
const expected = [
|
||||||
|
|
|
@ -38,10 +38,15 @@ const commandDefinitions = unmodifiedCommandDefinitions.filter(
|
||||||
({ name, hidden }) => !hidden && name !== 'rrf'
|
({ name, hidden }) => !hidden && name !== 'rrf'
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRecommendedQueriesSuggestionsFromTemplates = (fromCommand: string, timeField?: string) =>
|
const getRecommendedQueriesSuggestionsFromTemplates = (
|
||||||
|
fromCommand: string,
|
||||||
|
timeField?: string,
|
||||||
|
categorizationField?: string
|
||||||
|
) =>
|
||||||
getRecommendedQueries({
|
getRecommendedQueries({
|
||||||
fromCommand,
|
fromCommand,
|
||||||
timeField,
|
timeField,
|
||||||
|
categorizationField,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('autocomplete', () => {
|
describe('autocomplete', () => {
|
||||||
|
@ -91,7 +96,8 @@ describe('autocomplete', () => {
|
||||||
describe('New command', () => {
|
describe('New command', () => {
|
||||||
const recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
const recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
'FROM logs*',
|
'FROM logs*',
|
||||||
'dateField'
|
'dateField',
|
||||||
|
'textField'
|
||||||
);
|
);
|
||||||
testSuggestions('/', [
|
testSuggestions('/', [
|
||||||
...sourceCommands.map((name) => name.toUpperCase() + ' '),
|
...sourceCommands.map((name) => name.toUpperCase() + ' '),
|
||||||
|
@ -251,7 +257,8 @@ describe('autocomplete', () => {
|
||||||
// source command
|
// source command
|
||||||
let recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
let recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
'FROM logs*',
|
'FROM logs*',
|
||||||
'dateField'
|
'dateField',
|
||||||
|
'textField'
|
||||||
);
|
);
|
||||||
testSuggestions('f/', [
|
testSuggestions('f/', [
|
||||||
...sourceCommands.map((cmd) => `${cmd.toUpperCase()} `),
|
...sourceCommands.map((cmd) => `${cmd.toUpperCase()} `),
|
||||||
|
@ -477,7 +484,8 @@ describe('autocomplete', () => {
|
||||||
});
|
});
|
||||||
let recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
let recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
'FROM logs*',
|
'FROM logs*',
|
||||||
'dateField'
|
'dateField',
|
||||||
|
'textField'
|
||||||
);
|
);
|
||||||
// Source command
|
// Source command
|
||||||
testSuggestions('F/', [
|
testSuggestions('F/', [
|
||||||
|
@ -569,7 +577,11 @@ describe('autocomplete', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates('', 'dateField');
|
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
|
'',
|
||||||
|
'dateField',
|
||||||
|
'textField'
|
||||||
|
);
|
||||||
|
|
||||||
// PIPE (|)
|
// PIPE (|)
|
||||||
testSuggestions('FROM a /', [
|
testSuggestions('FROM a /', [
|
||||||
|
@ -620,7 +632,8 @@ describe('autocomplete', () => {
|
||||||
);
|
);
|
||||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
'index1',
|
'index1',
|
||||||
'dateField'
|
'dateField',
|
||||||
|
'textField'
|
||||||
);
|
);
|
||||||
|
|
||||||
testSuggestions(
|
testSuggestions(
|
||||||
|
@ -643,7 +656,8 @@ describe('autocomplete', () => {
|
||||||
|
|
||||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
'index2',
|
'index2',
|
||||||
'dateField'
|
'dateField',
|
||||||
|
'textField'
|
||||||
);
|
);
|
||||||
testSuggestions(
|
testSuggestions(
|
||||||
'FROM index1, index2/',
|
'FROM index1, index2/',
|
||||||
|
@ -669,7 +683,8 @@ describe('autocomplete', () => {
|
||||||
// we're setting it ourselves.
|
// we're setting it ourselves.
|
||||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
'foo$bar',
|
'foo$bar',
|
||||||
'dateField'
|
'dateField',
|
||||||
|
'textField'
|
||||||
);
|
);
|
||||||
testSuggestions(
|
testSuggestions(
|
||||||
'FROM foo$bar/',
|
'FROM foo$bar/',
|
||||||
|
@ -701,7 +716,8 @@ describe('autocomplete', () => {
|
||||||
// This is an identifier that matches multiple sources
|
// This is an identifier that matches multiple sources
|
||||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
'i*',
|
'i*',
|
||||||
'dateField'
|
'dateField',
|
||||||
|
'textField'
|
||||||
);
|
);
|
||||||
testSuggestions(
|
testSuggestions(
|
||||||
'FROM i*/',
|
'FROM i*/',
|
||||||
|
@ -722,7 +738,11 @@ describe('autocomplete', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates('', 'dateField');
|
recommendedQuerySuggestions = getRecommendedQueriesSuggestionsFromTemplates(
|
||||||
|
'',
|
||||||
|
'dateField',
|
||||||
|
'textField'
|
||||||
|
);
|
||||||
// FROM source METADATA
|
// FROM source METADATA
|
||||||
testSuggestions('FROM index1 M/', [attachTriggerCommand('METADATA ')]);
|
testSuggestions('FROM index1 M/', [attachTriggerCommand('METADATA ')]);
|
||||||
|
|
||||||
|
|
|
@ -9,22 +9,36 @@
|
||||||
import type { RecommendedQuery } from '@kbn/esql-types';
|
import type { RecommendedQuery } from '@kbn/esql-types';
|
||||||
import type { SuggestionRawDefinition, GetColumnsByTypeFn } from '../types';
|
import type { SuggestionRawDefinition, GetColumnsByTypeFn } from '../types';
|
||||||
import { getRecommendedQueries } from './templates';
|
import { getRecommendedQueries } from './templates';
|
||||||
|
import { METADATA_FIELDS } from '../../shared/constants';
|
||||||
|
|
||||||
export const getRecommendedQueriesSuggestionsFromStaticTemplates = async (
|
export const getRecommendedQueriesSuggestionsFromStaticTemplates = async (
|
||||||
getFieldsByType: GetColumnsByTypeFn,
|
getFieldsByType: GetColumnsByTypeFn,
|
||||||
fromCommand: string = ''
|
fromCommand: string = ''
|
||||||
): Promise<SuggestionRawDefinition[]> => {
|
): Promise<SuggestionRawDefinition[]> => {
|
||||||
const fieldSuggestions = await getFieldsByType('date', [], {
|
const [fieldSuggestions, textFieldSuggestions] = await Promise.all([
|
||||||
openSuggestions: true,
|
getFieldsByType(['date'], [], { openSuggestions: true }),
|
||||||
});
|
// get text fields separately to avoid mixing them with date fields
|
||||||
|
getFieldsByType(['text'], [], { openSuggestions: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
let timeField = '';
|
let timeField = '';
|
||||||
|
let categorizationField: string | undefined = '';
|
||||||
|
|
||||||
if (fieldSuggestions.length) {
|
if (fieldSuggestions.length) {
|
||||||
timeField =
|
timeField =
|
||||||
fieldSuggestions?.find((field) => field.label === '@timestamp')?.label ||
|
fieldSuggestions?.find((field) => field.label === '@timestamp')?.label ||
|
||||||
fieldSuggestions[0].label;
|
fieldSuggestions[0].label;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recommendedQueries = getRecommendedQueries({ fromCommand, timeField });
|
if (textFieldSuggestions.length) {
|
||||||
|
categorizationField = getCategorizationField(textFieldSuggestions.map((field) => field.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
const recommendedQueries = getRecommendedQueries({
|
||||||
|
fromCommand,
|
||||||
|
timeField,
|
||||||
|
categorizationField,
|
||||||
|
});
|
||||||
|
|
||||||
const suggestions: SuggestionRawDefinition[] = recommendedQueries.map((query) => {
|
const suggestions: SuggestionRawDefinition[] = recommendedQueries.map((query) => {
|
||||||
return {
|
return {
|
||||||
|
@ -95,3 +109,32 @@ export const getRecommendedQueriesTemplatesFromExtensions = (
|
||||||
|
|
||||||
return recommendedQueriesTemplates;
|
return recommendedQueriesTemplates;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns the categorization field from the list of fields.
|
||||||
|
* It checks for the presence of 'message', 'error.message', or 'event.original' in that order.
|
||||||
|
* If none of these fields are present, it returns the first field from the list,
|
||||||
|
* Assumes text fields have been passed in the `fields` array.
|
||||||
|
*
|
||||||
|
* This function is a duplicate of the one in src/platform/packages/shared/kbn-aiops-utils.
|
||||||
|
* It is included here to avoid build errors due to bazel
|
||||||
|
*
|
||||||
|
* TODO: Remove this function once the bazel issue is resolved.
|
||||||
|
*
|
||||||
|
* @param fields, the list of fields to check
|
||||||
|
* @returns string | undefined, the categorization field if found, otherwise undefined
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getCategorizationField(fields: string[]): string | undefined {
|
||||||
|
const fieldPriority = ['message', 'error.message', 'event.original'];
|
||||||
|
const fieldSet = new Set(fields);
|
||||||
|
for (const field of fieldPriority) {
|
||||||
|
if (fieldSet.has(field)) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out metadata fields
|
||||||
|
const filteredFields = fields.filter((field) => !METADATA_FIELDS.includes(field));
|
||||||
|
return filteredFields[0] ?? undefined;
|
||||||
|
}
|
||||||
|
|
|
@ -14,9 +14,11 @@ import { i18n } from '@kbn/i18n';
|
||||||
export const getRecommendedQueries = ({
|
export const getRecommendedQueries = ({
|
||||||
fromCommand,
|
fromCommand,
|
||||||
timeField,
|
timeField,
|
||||||
|
categorizationField,
|
||||||
}: {
|
}: {
|
||||||
fromCommand: string;
|
fromCommand: string;
|
||||||
timeField?: string;
|
timeField?: string;
|
||||||
|
categorizationField?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const queries = [
|
const queries = [
|
||||||
{
|
{
|
||||||
|
@ -135,6 +137,8 @@ export const getRecommendedQueries = ({
|
||||||
label: i18n.translate(
|
label: i18n.translate(
|
||||||
'kbn-esql-validation-autocomplete.recommendedQueries.categorize.label',
|
'kbn-esql-validation-autocomplete.recommendedQueries.categorize.label',
|
||||||
{
|
{
|
||||||
|
// TODO this item should be hidden if AIOps is disabled or we're not running with a platinum license
|
||||||
|
// the capability aiops.enabled can be used to check both of these conditions
|
||||||
defaultMessage: 'Detect change points',
|
defaultMessage: 'Detect change points',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -170,6 +174,27 @@ export const getRecommendedQueries = ({
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(categorizationField
|
||||||
|
? // TODO this item should be hidden if AIOps is disabled or we're not running with a platinum license
|
||||||
|
// the capability aiops.enabled can be used to check both of these conditions
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: i18n.translate(
|
||||||
|
'kbn-esql-validation-autocomplete.recommendedQueries.patternAnalysis.label',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Identify patterns',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description: i18n.translate(
|
||||||
|
'kbn-esql-validation-autocomplete.recommendedQueries.patternAnalysis.description',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Use the CATEGORIZE function to identify patterns in your logs',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
queryString: `${fromCommand}\n | STATS Count=COUNT(*) BY Pattern=CATEGORIZE(${categorizationField})\n | SORT Count DESC`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
return queries;
|
return queries;
|
||||||
};
|
};
|
||||||
|
|
|
@ -297,12 +297,14 @@ describe('Test discover app state container', () => {
|
||||||
const state = getStateContainer();
|
const state = getStateContainer();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: false,
|
columns: false,
|
||||||
|
hideChart: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
});
|
});
|
||||||
state.initAndSync();
|
state.initAndSync();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: true,
|
columns: true,
|
||||||
|
hideChart: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
});
|
});
|
||||||
|
@ -314,12 +316,14 @@ describe('Test discover app state container', () => {
|
||||||
const state = getStateContainer();
|
const state = getStateContainer();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: false,
|
columns: false,
|
||||||
|
hideChart: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
});
|
});
|
||||||
state.initAndSync();
|
state.initAndSync();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: false,
|
columns: false,
|
||||||
|
hideChart: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
});
|
});
|
||||||
|
@ -331,17 +335,38 @@ describe('Test discover app state container', () => {
|
||||||
const state = getStateContainer();
|
const state = getStateContainer();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: false,
|
columns: false,
|
||||||
|
hideChart: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
});
|
});
|
||||||
state.initAndSync();
|
state.initAndSync();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: true,
|
columns: true,
|
||||||
|
hideChart: true,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call setResetDefaultProfileState correctly with initial hide chart', () => {
|
||||||
|
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
|
||||||
|
stateStorageGetSpy.mockReturnValue({ hideChart: true });
|
||||||
|
const state = getStateContainer();
|
||||||
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
|
columns: false,
|
||||||
|
hideChart: false,
|
||||||
|
rowHeight: false,
|
||||||
|
breakdownField: false,
|
||||||
|
});
|
||||||
|
state.initAndSync();
|
||||||
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
|
columns: true,
|
||||||
|
hideChart: false,
|
||||||
|
rowHeight: true,
|
||||||
|
breakdownField: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should call setResetDefaultProfileState correctly with saved search', () => {
|
it('should call setResetDefaultProfileState correctly with saved search', () => {
|
||||||
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
|
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
|
||||||
stateStorageGetSpy.mockReturnValue({ columns: ['test'], rowHeight: 5 });
|
stateStorageGetSpy.mockReturnValue({ columns: ['test'], rowHeight: 5 });
|
||||||
|
@ -354,12 +379,14 @@ describe('Test discover app state container', () => {
|
||||||
const state = getStateContainer();
|
const state = getStateContainer();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: false,
|
columns: false,
|
||||||
|
hideChart: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
});
|
});
|
||||||
state.initAndSync();
|
state.initAndSync();
|
||||||
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: false,
|
columns: false,
|
||||||
|
hideChart: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -271,7 +271,10 @@ export const getDiscoverAppStateContainer = ({
|
||||||
// Set the default profile state only if not loading a saved search,
|
// Set the default profile state only if not loading a saved search,
|
||||||
// to avoid overwriting saved search state
|
// to avoid overwriting saved search state
|
||||||
if (!currentSavedSearch.id) {
|
if (!currentSavedSearch.id) {
|
||||||
const { breakdownField, columns, rowHeight } = getCurrentUrlState(stateStorage, services);
|
const { breakdownField, columns, rowHeight, hideChart } = getCurrentUrlState(
|
||||||
|
stateStorage,
|
||||||
|
services
|
||||||
|
);
|
||||||
|
|
||||||
// Only set default state which is not already set in the URL
|
// Only set default state which is not already set in the URL
|
||||||
internalState.dispatch(
|
internalState.dispatch(
|
||||||
|
@ -280,6 +283,7 @@ export const getDiscoverAppStateContainer = ({
|
||||||
columns: columns === undefined,
|
columns: columns === undefined,
|
||||||
rowHeight: rowHeight === undefined,
|
rowHeight: rowHeight === undefined,
|
||||||
breakdownField: breakdownField === undefined,
|
breakdownField: breakdownField === undefined,
|
||||||
|
hideChart: hideChart === undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -183,6 +183,7 @@ describe('test getDataStateContainer', () => {
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -200,6 +201,7 @@ describe('test getDataStateContainer', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
});
|
});
|
||||||
expect(stateContainer.appState.get().columns).toEqual(['message', 'extension']);
|
expect(stateContainer.appState.get().columns).toEqual(['message', 'extension']);
|
||||||
expect(stateContainer.appState.get().rowHeight).toEqual(3);
|
expect(stateContainer.appState.get().rowHeight).toEqual(3);
|
||||||
|
@ -225,6 +227,7 @@ describe('test getDataStateContainer', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -240,6 +243,7 @@ describe('test getDataStateContainer', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
});
|
});
|
||||||
expect(stateContainer.appState.get().columns).toEqual(['default_column']);
|
expect(stateContainer.appState.get().columns).toEqual(['default_column']);
|
||||||
expect(stateContainer.appState.get().rowHeight).toBeUndefined();
|
expect(stateContainer.appState.get().rowHeight).toBeUndefined();
|
||||||
|
|
|
@ -359,6 +359,7 @@ export function getDataStateContainer({
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -55,6 +55,7 @@ export const defaultTabState: Omit<TabState, keyof TabItem> = {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
documentsRequest: {
|
documentsRequest: {
|
||||||
loadingStatus: LoadingStatus.Uninitialized,
|
loadingStatus: LoadingStatus.Uninitialized,
|
||||||
|
|
|
@ -70,6 +70,7 @@ export interface TabState extends TabItem {
|
||||||
columns: boolean;
|
columns: boolean;
|
||||||
rowHeight: boolean;
|
rowHeight: boolean;
|
||||||
breakdownField: boolean;
|
breakdownField: boolean;
|
||||||
|
hideChart: boolean;
|
||||||
};
|
};
|
||||||
documentsRequest: DocumentsRequest;
|
documentsRequest: DocumentsRequest;
|
||||||
totalHitsRequest: TotalHitsRequest;
|
totalHitsRequest: TotalHitsRequest;
|
||||||
|
|
|
@ -492,6 +492,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
const documents$ = stateContainer.dataState.data$.documents$;
|
const documents$ = stateContainer.dataState.data$.documents$;
|
||||||
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: false,
|
columns: false,
|
||||||
|
hideChart: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
});
|
});
|
||||||
|
@ -507,6 +508,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
|
||||||
columns: true,
|
columns: true,
|
||||||
|
hideChart: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
})
|
})
|
||||||
|
@ -521,6 +523,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -534,6 +537,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
documents$.next({
|
documents$.next({
|
||||||
|
@ -550,6 +554,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
|
hideChart: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
documents$.next({
|
documents$.next({
|
||||||
|
@ -567,6 +572,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
});
|
});
|
||||||
documents$.next({
|
documents$.next({
|
||||||
fetchStatus: FetchStatus.PARTIAL,
|
fetchStatus: FetchStatus.PARTIAL,
|
||||||
|
@ -578,6 +584,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
documents$.next({
|
documents$.next({
|
||||||
|
@ -590,6 +597,7 @@ describe('buildEsqlFetchSubscribe', () => {
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -98,6 +98,7 @@ export const buildEsqlFetchSubscribe = ({
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
|
hideChart: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -158,6 +159,7 @@ export const buildEsqlFetchSubscribe = ({
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -84,6 +84,7 @@ describe('changeDataView', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await changeDataView({ dataViewId: dataViewComplexMock.id!, ...params });
|
await changeDataView({ dataViewId: dataViewComplexMock.id!, ...params });
|
||||||
|
@ -92,6 +93,7 @@ describe('changeDataView', () => {
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
|
hideChart: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -82,6 +82,7 @@ export async function changeDataView({
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
|
hideChart: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,6 +32,7 @@ describe('getDefaultProfileState', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
dataView: dataViewWithTimefieldMock,
|
dataView: dataViewWithTimefieldMock,
|
||||||
}).getPreFetchState();
|
}).getPreFetchState();
|
||||||
|
@ -45,6 +46,7 @@ describe('getDefaultProfileState', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: true,
|
breakdownField: true,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
dataView: emptyDataView,
|
dataView: emptyDataView,
|
||||||
}).getPreFetchState();
|
}).getPreFetchState();
|
||||||
|
@ -61,6 +63,7 @@ describe('getDefaultProfileState', () => {
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
dataView: dataViewWithTimefieldMock,
|
dataView: dataViewWithTimefieldMock,
|
||||||
}).getPostFetchState({
|
}).getPostFetchState({
|
||||||
|
@ -87,6 +90,7 @@ describe('getDefaultProfileState', () => {
|
||||||
columns: true,
|
columns: true,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
dataView: emptyDataView,
|
dataView: emptyDataView,
|
||||||
}).getPostFetchState({
|
}).getPostFetchState({
|
||||||
|
@ -116,6 +120,7 @@ describe('getDefaultProfileState', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: true,
|
rowHeight: true,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
dataView: dataViewWithTimefieldMock,
|
dataView: dataViewWithTimefieldMock,
|
||||||
}).getPostFetchState({
|
}).getPostFetchState({
|
||||||
|
@ -135,6 +140,7 @@ describe('getDefaultProfileState', () => {
|
||||||
columns: false,
|
columns: false,
|
||||||
rowHeight: false,
|
rowHeight: false,
|
||||||
breakdownField: false,
|
breakdownField: false,
|
||||||
|
hideChart: false,
|
||||||
},
|
},
|
||||||
dataView: dataViewWithTimefieldMock,
|
dataView: dataViewWithTimefieldMock,
|
||||||
}).getPostFetchState({
|
}).getPostFetchState({
|
||||||
|
|
|
@ -44,6 +44,10 @@ export const getDefaultProfileState = ({
|
||||||
stateUpdate.breakdownField = defaultState.breakdownField;
|
stateUpdate.breakdownField = defaultState.breakdownField;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (defaultState.hideChart !== undefined) {
|
||||||
|
stateUpdate.hideChart = defaultState.hideChart;
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(stateUpdate).length ? stateUpdate : undefined;
|
return Object.keys(stateUpdate).length ? stateUpdate : undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { createPatternDataSourceProfileProvider } from './profile';
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { UseEuiTheme } from '@elastic/eui';
|
||||||
|
import { EuiCode, EuiSpacer, EuiText } from '@elastic/eui';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import type { DataTableRecord } from '@kbn/discover-utils';
|
||||||
|
import { extractCategorizeTokens } from '@kbn/esql-utils';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
|
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pattern: string;
|
||||||
|
isDetails: boolean;
|
||||||
|
defaultRowHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PatternCellRenderer: FC<Props> = ({ pattern, isDetails, defaultRowHeight }) => {
|
||||||
|
const styles = useMemoCss(componentStyles);
|
||||||
|
|
||||||
|
const keywords = useMemo(() => extractCategorizeTokens(pattern), [pattern]);
|
||||||
|
const containerStyle = useMemo(() => getContainerStyle(defaultRowHeight), [defaultRowHeight]);
|
||||||
|
|
||||||
|
const formattedTokens = useMemo(
|
||||||
|
() =>
|
||||||
|
keywords.map((keyword, index) => {
|
||||||
|
return (
|
||||||
|
<EuiCode key={index} css={styles.keyword}>
|
||||||
|
{keyword}
|
||||||
|
</EuiCode>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[styles, keywords]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDetails) {
|
||||||
|
return (
|
||||||
|
<div css={styles.detailsContainer}>
|
||||||
|
<EuiText size="s">
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id="discover.contextAwareness.patternCellRenderer.tokensLabel"
|
||||||
|
defaultMessage="Tokens"
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
{formattedTokens}
|
||||||
|
</EuiText>
|
||||||
|
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
|
<EuiText size="s">
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id="discover.contextAwareness.patternCellRenderer.regexLabel"
|
||||||
|
defaultMessage="Regex"
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<span data-test-subj="euiDataGridExpansionPopover-patternRegex">{pattern}</span>
|
||||||
|
</EuiText>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div css={containerStyle}>{formattedTokens}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentStyles = {
|
||||||
|
keyword: ({ euiTheme }: UseEuiTheme) =>
|
||||||
|
css({
|
||||||
|
marginRight: euiTheme.size.xs,
|
||||||
|
marginBottom: `calc(${euiTheme.size.m} / 2)`,
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: `${euiTheme.size.xxs} ${euiTheme.size.s}`,
|
||||||
|
backgroundColor: euiTheme.colors.lightestShade,
|
||||||
|
borderRadius: euiTheme.border.radius.small,
|
||||||
|
color: euiTheme.colors.textPrimary,
|
||||||
|
fontSize: euiTheme.size.m,
|
||||||
|
}),
|
||||||
|
detailsContainer: ({ euiTheme }: UseEuiTheme) =>
|
||||||
|
css({
|
||||||
|
maxWidth: '600px',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPatternCellRenderer(
|
||||||
|
row: DataTableRecord,
|
||||||
|
columnId: string,
|
||||||
|
isDetails: boolean,
|
||||||
|
defaultRowHeight?: number
|
||||||
|
) {
|
||||||
|
const pattern = row.flattened[columnId];
|
||||||
|
if (pattern === undefined) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PatternCellRenderer
|
||||||
|
pattern={String(pattern)}
|
||||||
|
isDetails={isDetails}
|
||||||
|
defaultRowHeight={defaultRowHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContainerStyle(defaultRowHeight?: number) {
|
||||||
|
// the keywords are slightly larger than the default text height,
|
||||||
|
// so they need to be adjusted to fit within the row height while
|
||||||
|
// not truncating the bottom of the text
|
||||||
|
let rowHeight = 2;
|
||||||
|
if (defaultRowHeight === undefined) {
|
||||||
|
rowHeight = 2;
|
||||||
|
} else if (defaultRowHeight < 2) {
|
||||||
|
rowHeight = 1;
|
||||||
|
} else {
|
||||||
|
rowHeight = Math.floor(defaultRowHeight / 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical' as const,
|
||||||
|
WebkitLineClamp: rowHeight,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||||
|
import { extractCategorizeTokens, getCategorizeColumns, getCategorizeField } from '@kbn/esql-utils';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
|
||||||
|
import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources';
|
||||||
|
import type { DataSourceProfileProvider } from '../../../profiles';
|
||||||
|
import { DataSourceCategory } from '../../../profiles';
|
||||||
|
import { getPatternCellRenderer } from './pattern_cell_renderer';
|
||||||
|
import type { ProfileProviderServices } from '../../profile_provider_services';
|
||||||
|
|
||||||
|
const DOC_LIMIT = 10000;
|
||||||
|
|
||||||
|
export const createPatternDataSourceProfileProvider = (
|
||||||
|
services: ProfileProviderServices
|
||||||
|
): DataSourceProfileProvider<{
|
||||||
|
patternColumns: string[];
|
||||||
|
}> => ({
|
||||||
|
profileId: 'patterns-data-source-profile',
|
||||||
|
profile: {
|
||||||
|
getCellRenderers:
|
||||||
|
(prev, { context }) =>
|
||||||
|
(params) => {
|
||||||
|
const { rowHeight } = params;
|
||||||
|
const { patternColumns } = context;
|
||||||
|
if (!patternColumns || patternColumns.length === 0) {
|
||||||
|
return prev(params);
|
||||||
|
}
|
||||||
|
const patternRenderers = patternColumns.reduce(
|
||||||
|
(acc, column) =>
|
||||||
|
Object.assign(acc, {
|
||||||
|
[column]: (props: DataGridCellValueElementProps) =>
|
||||||
|
getPatternCellRenderer(props.row, props.columnId, props.isDetails, rowHeight),
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev(params),
|
||||||
|
...patternRenderers,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getAdditionalCellActions:
|
||||||
|
(prev, { context }) =>
|
||||||
|
() => {
|
||||||
|
return [
|
||||||
|
...prev(),
|
||||||
|
{
|
||||||
|
id: 'patterns-action-view-docs-in-discover',
|
||||||
|
getDisplayName: () =>
|
||||||
|
i18n.translate('discover.docViews.patterns.cellAction.viewResults', {
|
||||||
|
defaultMessage: 'View matching results',
|
||||||
|
}),
|
||||||
|
getIconType: () => 'discoverApp',
|
||||||
|
isCompatible: (compatibleContext) => {
|
||||||
|
const { query, field } = compatibleContext;
|
||||||
|
const { patternColumns } = context;
|
||||||
|
if (!isOfAggregateQueryType(query) || field === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return patternColumns.includes(field.name);
|
||||||
|
},
|
||||||
|
execute: (executeContext) => {
|
||||||
|
const index = executeContext.dataView?.getIndexPattern();
|
||||||
|
if (
|
||||||
|
!isOfAggregateQueryType(executeContext.query) ||
|
||||||
|
!executeContext.value ||
|
||||||
|
!index
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = extractCategorizeTokens(executeContext.value as string).join(' ');
|
||||||
|
const categoryField = getCategorizeField(executeContext.query.esql);
|
||||||
|
|
||||||
|
if (!categoryField || !pattern) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
...executeContext.query,
|
||||||
|
esql: `FROM ${index}\n | WHERE MATCH(${categoryField}, "${pattern}", {"auto_generate_synonyms_phrase_query": false, "fuzziness": 0, "operator": "AND"})\n | LIMIT ${DOC_LIMIT}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const discoverLink = services.locator.getRedirectUrl({
|
||||||
|
query,
|
||||||
|
timeRange: executeContext.timeRange,
|
||||||
|
hideChart: false,
|
||||||
|
});
|
||||||
|
window.open(discoverLink, '_blank');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
getDefaultAppState: (prev) => (params) => {
|
||||||
|
return {
|
||||||
|
...prev(params),
|
||||||
|
hideChart: true,
|
||||||
|
columns: [
|
||||||
|
{ name: 'Count', width: 150 },
|
||||||
|
{ name: 'Pattern', width: undefined },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: (params) => {
|
||||||
|
if (!isDataSourceType(params.dataSource, DataSourceType.Esql)) {
|
||||||
|
return { isMatch: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = params.query;
|
||||||
|
|
||||||
|
if (!isOfAggregateQueryType(query)) {
|
||||||
|
return { isMatch: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const patternColumns = getCategorizeColumns(query.esql);
|
||||||
|
if (patternColumns.length === 0) {
|
||||||
|
return { isMatch: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMatch: true,
|
||||||
|
context: {
|
||||||
|
category: DataSourceCategory.Default,
|
||||||
|
patternColumns,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -29,6 +29,7 @@ import { createTracesDataSourceProfileProvider } from './observability/traces_da
|
||||||
import { createDeprecationLogsDataSourceProfileProvider } from './common/deprecation_logs';
|
import { createDeprecationLogsDataSourceProfileProvider } from './common/deprecation_logs';
|
||||||
import { createClassicNavRootProfileProvider } from './common/classic_nav_root_profile';
|
import { createClassicNavRootProfileProvider } from './common/classic_nav_root_profile';
|
||||||
import { createObservabilityDocumentProfileProviders } from './observability/observability_profile_providers';
|
import { createObservabilityDocumentProfileProviders } from './observability/observability_profile_providers';
|
||||||
|
import { createPatternDataSourceProfileProvider } from './common/patterns';
|
||||||
import { createSecurityDocumentProfileProvider } from './security/security_document_profile';
|
import { createSecurityDocumentProfileProvider } from './security/security_document_profile';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,6 +148,7 @@ const createRootProfileProviders = (providerServices: ProfileProviderServices) =
|
||||||
*/
|
*/
|
||||||
const createDataSourceProfileProviders = (providerServices: ProfileProviderServices) => [
|
const createDataSourceProfileProviders = (providerServices: ProfileProviderServices) => [
|
||||||
createExampleDataSourceProfileProvider(),
|
createExampleDataSourceProfileProvider(),
|
||||||
|
createPatternDataSourceProfileProvider(providerServices),
|
||||||
createDeprecationLogsDataSourceProfileProvider(),
|
createDeprecationLogsDataSourceProfileProvider(),
|
||||||
createTracesDataSourceProfileProvider(providerServices),
|
createTracesDataSourceProfileProvider(providerServices),
|
||||||
...createObservabilityLogsDataSourceProfileProviders(providerServices),
|
...createObservabilityLogsDataSourceProfileProviders(providerServices),
|
||||||
|
|
|
@ -140,6 +140,10 @@ export interface DefaultAppStateExtension {
|
||||||
* The field to apply for the histogram breakdown
|
* The field to apply for the histogram breakdown
|
||||||
*/
|
*/
|
||||||
breakdownField?: string;
|
breakdownField?: string;
|
||||||
|
/**
|
||||||
|
* The state for chart visibility toggle
|
||||||
|
*/
|
||||||
|
hideChart?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { BehaviorSubject } from 'rxjs';
|
||||||
import { screen, render, waitFor } from '@testing-library/react';
|
import { screen, render, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
|
import { createStubDataView, stubLogstashFieldSpecMap } from '@kbn/data-views-plugin/common/stubs';
|
||||||
import { stubIndexPattern } from '@kbn/data-plugin/public/stubs';
|
import { stubIndexPattern } from '@kbn/data-plugin/public/stubs';
|
||||||
import { coreMock } from '@kbn/core/public/mocks';
|
import { coreMock } from '@kbn/core/public/mocks';
|
||||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||||
|
@ -95,6 +96,7 @@ describe('ESQLMenuPopover', () => {
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Count of logs')).toBeInTheDocument();
|
expect(screen.getByText('Count of logs')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Average bytes')).toBeInTheDocument();
|
expect(screen.getByText('Average bytes')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Identify patterns')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,4 +115,40 @@ describe('ESQLMenuPopover', () => {
|
||||||
await userEvent.click(screen.getByRole('button'));
|
await userEvent.click(screen.getByRole('button'));
|
||||||
expect(screen.queryByTestId('esql-recommended-queries')).toBeInTheDocument();
|
expect(screen.queryByTestId('esql-recommended-queries')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show identify patterns recommended query', async () => {
|
||||||
|
const stubLogstashDataView = createStubDataView({
|
||||||
|
spec: {
|
||||||
|
id: 'logstash-*',
|
||||||
|
title: 'logstash-*',
|
||||||
|
timeFieldName: 'time',
|
||||||
|
fields: {
|
||||||
|
...stubLogstashFieldSpecMap,
|
||||||
|
message: {
|
||||||
|
name: 'message',
|
||||||
|
type: 'string',
|
||||||
|
esTypes: ['text'],
|
||||||
|
aggregatable: true,
|
||||||
|
searchable: true,
|
||||||
|
count: 0,
|
||||||
|
readFromDocValues: true,
|
||||||
|
scripted: false,
|
||||||
|
isMapped: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderESQLPopover(stubLogstashDataView);
|
||||||
|
|
||||||
|
// open the popover and check for recommended queries
|
||||||
|
await userEvent.click(screen.getByRole('button'));
|
||||||
|
expect(screen.queryByTestId('esql-recommended-queries')).toBeInTheDocument();
|
||||||
|
// Open the nested section to see the recommended queries
|
||||||
|
await waitFor(() => userEvent.click(screen.getByTestId('esql-recommended-queries')));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Identify patterns')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
getRecommendedQueriesTemplatesFromExtensions,
|
getRecommendedQueriesTemplatesFromExtensions,
|
||||||
} from '@kbn/esql-validation-autocomplete';
|
} from '@kbn/esql-validation-autocomplete';
|
||||||
import { LanguageDocumentationFlyout } from '@kbn/language-documentation';
|
import { LanguageDocumentationFlyout } from '@kbn/language-documentation';
|
||||||
|
import { getCategorizationField } from '@kbn/aiops-utils';
|
||||||
import type { IUnifiedSearchPluginServices } from '../types';
|
import type { IUnifiedSearchPluginServices } from '../types';
|
||||||
|
|
||||||
export interface ESQLMenuPopoverProps {
|
export interface ESQLMenuPopoverProps {
|
||||||
|
@ -52,17 +53,25 @@ export const ESQLMenuPopover: React.FC<ESQLMenuPopoverProps> = ({
|
||||||
RecommendedQuery[]
|
RecommendedQuery[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const { queryForRecommendedQueries, timeFieldName } = useMemo(() => {
|
const { queryForRecommendedQueries, timeFieldName, categorizationField } = useMemo(() => {
|
||||||
if (adHocDataview && typeof adHocDataview !== 'string') {
|
if (adHocDataview && typeof adHocDataview !== 'string') {
|
||||||
|
const textFields = adHocDataview.fields?.getByType('string') ?? [];
|
||||||
|
let tempCategorizationField;
|
||||||
|
if (textFields.length) {
|
||||||
|
tempCategorizationField = getCategorizationField(textFields.map((field) => field.name));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queryForRecommendedQueries: `FROM ${adHocDataview.name}`,
|
queryForRecommendedQueries: `FROM ${adHocDataview.name}`,
|
||||||
timeFieldName:
|
timeFieldName:
|
||||||
adHocDataview.timeFieldName ?? adHocDataview.fields?.getByType('date')?.[0]?.name,
|
adHocDataview.timeFieldName ?? adHocDataview.fields?.getByType('date')?.[0]?.name,
|
||||||
|
categorizationField: tempCategorizationField,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
queryForRecommendedQueries: '',
|
queryForRecommendedQueries: '',
|
||||||
timeFieldName: undefined,
|
timeFieldName: undefined,
|
||||||
|
categorizationField: undefined,
|
||||||
};
|
};
|
||||||
}, [adHocDataview]);
|
}, [adHocDataview]);
|
||||||
|
|
||||||
|
@ -130,11 +139,13 @@ export const ESQLMenuPopover: React.FC<ESQLMenuPopoverProps> = ({
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the static recommended queries, no solutions specific
|
// Handle the static recommended queries, no solutions specific
|
||||||
if (queryForRecommendedQueries && timeFieldName) {
|
if (queryForRecommendedQueries && timeFieldName) {
|
||||||
const recommendedQueriesFromStaticTemplates = getRecommendedQueries({
|
const recommendedQueriesFromStaticTemplates = getRecommendedQueries({
|
||||||
fromCommand: queryForRecommendedQueries,
|
fromCommand: queryForRecommendedQueries,
|
||||||
timeField: timeFieldName,
|
timeField: timeFieldName,
|
||||||
|
categorizationField,
|
||||||
});
|
});
|
||||||
|
|
||||||
recommendedQueries.push(...recommendedQueriesFromStaticTemplates);
|
recommendedQueries.push(...recommendedQueriesFromStaticTemplates);
|
||||||
|
@ -239,6 +250,7 @@ export const ESQLMenuPopover: React.FC<ESQLMenuPopoverProps> = ({
|
||||||
timeFieldName,
|
timeFieldName,
|
||||||
toggleLanguageComponent,
|
toggleLanguageComponent,
|
||||||
solutionsRecommendedQueries, // This dependency is fine here, as it *uses* the state
|
solutionsRecommendedQueries, // This dependency is fine here, as it *uses* the state
|
||||||
|
categorizationField,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const esqlMenuPopoverStyles = css`
|
const esqlMenuPopoverStyles = css`
|
||||||
|
|
|
@ -50,7 +50,8 @@
|
||||||
"@kbn/react-kibana-mount",
|
"@kbn/react-kibana-mount",
|
||||||
"@kbn/field-utils",
|
"@kbn/field-utils",
|
||||||
"@kbn/language-documentation",
|
"@kbn/language-documentation",
|
||||||
"@kbn/esql-types"
|
"@kbn/esql-types",
|
||||||
|
"@kbn/aiops-utils",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -40,6 +40,8 @@
|
||||||
"@kbn/aiops-plugin/*": ["x-pack/platform/plugins/shared/aiops/*"],
|
"@kbn/aiops-plugin/*": ["x-pack/platform/plugins/shared/aiops/*"],
|
||||||
"@kbn/aiops-test-utils": ["x-pack/platform/packages/private/ml/aiops_test_utils"],
|
"@kbn/aiops-test-utils": ["x-pack/platform/packages/private/ml/aiops_test_utils"],
|
||||||
"@kbn/aiops-test-utils/*": ["x-pack/platform/packages/private/ml/aiops_test_utils/*"],
|
"@kbn/aiops-test-utils/*": ["x-pack/platform/packages/private/ml/aiops_test_utils/*"],
|
||||||
|
"@kbn/aiops-utils": ["src/platform/packages/shared/kbn-aiops-utils"],
|
||||||
|
"@kbn/aiops-utils/*": ["src/platform/packages/shared/kbn-aiops-utils/*"],
|
||||||
"@kbn/alerting-api-integration-helpers": ["x-pack/platform/test/alerting_api_integration/packages/helpers"],
|
"@kbn/alerting-api-integration-helpers": ["x-pack/platform/test/alerting_api_integration/packages/helpers"],
|
||||||
"@kbn/alerting-api-integration-helpers/*": ["x-pack/platform/test/alerting_api_integration/packages/helpers/*"],
|
"@kbn/alerting-api-integration-helpers/*": ["x-pack/platform/test/alerting_api_integration/packages/helpers/*"],
|
||||||
"@kbn/alerting-api-integration-test-plugin": ["x-pack/platform/test/alerting_api_integration/common/plugins/alerts"],
|
"@kbn/alerting-api-integration-test-plugin": ["x-pack/platform/test/alerting_api_integration/common/plugins/alerts"],
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { css } from '@emotion/react';
|
||||||
import { useTableState } from '@kbn/ml-in-memory-table/hooks/use_table_state';
|
import { useTableState } from '@kbn/ml-in-memory-table/hooks/use_table_state';
|
||||||
import useMountedState from 'react-use/lib/useMountedState';
|
import useMountedState from 'react-use/lib/useMountedState';
|
||||||
import { getEsQueryConfig } from '@kbn/data-service';
|
import { getEsQueryConfig } from '@kbn/data-service';
|
||||||
|
import { getCategorizationDataViewField } from '@kbn/aiops-utils';
|
||||||
import {
|
import {
|
||||||
type LogCategorizationPageUrlState,
|
type LogCategorizationPageUrlState,
|
||||||
getDefaultLogCategorizationAppState,
|
getDefaultLogCategorizationAppState,
|
||||||
|
@ -42,7 +43,7 @@ import { useValidateFieldRequest } from '../use_validate_category_field';
|
||||||
import { FieldValidationCallout } from '../category_validation_callout';
|
import { FieldValidationCallout } from '../category_validation_callout';
|
||||||
import { useMinimumTimeRange } from './use_minimum_time_range';
|
import { useMinimumTimeRange } from './use_minimum_time_range';
|
||||||
|
|
||||||
import { createAdditionalConfigHash, createDocumentStatsHash, getMessageField } from '../utils';
|
import { createAdditionalConfigHash, createDocumentStatsHash } from '../utils';
|
||||||
import { DiscoverTabs } from './discover_tabs';
|
import { DiscoverTabs } from './discover_tabs';
|
||||||
import { useRandomSamplerStorage } from '../sampling_menu';
|
import { useRandomSamplerStorage } from '../sampling_menu';
|
||||||
import { useActions } from '../category_table/use_actions';
|
import { useActions } from '../category_table/use_actions';
|
||||||
|
@ -126,7 +127,7 @@ export const LogCategorizationDiscover: FC<LogCategorizationEmbeddableProps> = (
|
||||||
setCurrentDocumentStatsHash(null);
|
setCurrentDocumentStatsHash(null);
|
||||||
setSelectedField(null);
|
setSelectedField(null);
|
||||||
setLoading(null);
|
setLoading(null);
|
||||||
const { dataViewFields, messageField } = getMessageField(dataView);
|
const { dataViewFields, messageField } = getCategorizationDataViewField(dataView);
|
||||||
setFields(dataViewFields);
|
setFields(dataViewFields);
|
||||||
setSelectedField(messageField);
|
setSelectedField(messageField);
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,8 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { stringHash } from '@kbn/ml-string-hash';
|
import { stringHash } from '@kbn/ml-string-hash';
|
||||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
|
||||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
|
||||||
import type { DocumentStats } from '../../hooks/use_document_count_stats';
|
import type { DocumentStats } from '../../hooks/use_document_count_stats';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,35 +24,3 @@ export function createDocumentStatsHash(documentStats: DocumentStats) {
|
||||||
export function createAdditionalConfigHash(additionalStrings: string[] = []) {
|
export function createAdditionalConfigHash(additionalStrings: string[] = []) {
|
||||||
return stringHash(`${additionalStrings.join('')}`);
|
return stringHash(`${additionalStrings.join('')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the message field from a DataView object.
|
|
||||||
* If the message field is not found, it falls back to error.message or event.original or the first text field in the DataView.
|
|
||||||
*
|
|
||||||
* @param dataView - The DataView object containing the fields.
|
|
||||||
* @returns An object containing the message field and all the fields in the DataView.
|
|
||||||
*/
|
|
||||||
export function getMessageField(dataView: DataView): {
|
|
||||||
messageField: DataViewField | null;
|
|
||||||
dataViewFields: DataViewField[];
|
|
||||||
} {
|
|
||||||
const dataViewFields = dataView.fields.filter((f) => f.esTypes?.includes(ES_FIELD_TYPES.TEXT));
|
|
||||||
|
|
||||||
let messageField: DataViewField | null | undefined = dataViewFields.find(
|
|
||||||
(f) => f.name === 'message'
|
|
||||||
);
|
|
||||||
if (messageField === undefined) {
|
|
||||||
messageField = dataViewFields.find((f) => f.name === 'error.message');
|
|
||||||
}
|
|
||||||
if (messageField === undefined) {
|
|
||||||
messageField = dataViewFields.find((f) => f.name === 'event.original');
|
|
||||||
}
|
|
||||||
if (messageField === undefined) {
|
|
||||||
if (dataViewFields.length > 0) {
|
|
||||||
messageField = dataViewFields[0];
|
|
||||||
} else {
|
|
||||||
messageField = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { messageField, dataViewFields };
|
|
||||||
}
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||||
import useMountedState from 'react-use/lib/useMountedState';
|
import useMountedState from 'react-use/lib/useMountedState';
|
||||||
|
import { getCategorizationDataViewField } from '@kbn/aiops-utils';
|
||||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||||
import { DataSourceContextProvider } from '../../hooks/use_data_source';
|
import { DataSourceContextProvider } from '../../hooks/use_data_source';
|
||||||
import type { PatternAnalysisEmbeddableRuntimeState } from './types';
|
import type { PatternAnalysisEmbeddableRuntimeState } from './types';
|
||||||
|
@ -43,7 +44,7 @@ import {
|
||||||
DEFAULT_MINIMUM_TIME_RANGE_OPTION,
|
DEFAULT_MINIMUM_TIME_RANGE_OPTION,
|
||||||
type MinimumTimeRangeOption,
|
type MinimumTimeRangeOption,
|
||||||
} from '../../components/log_categorization/log_categorization_for_embeddable/minimum_time_range';
|
} from '../../components/log_categorization/log_categorization_for_embeddable/minimum_time_range';
|
||||||
import { getMessageField } from '../../components/log_categorization/utils';
|
|
||||||
import { FieldSelector } from '../../components/log_categorization/log_categorization_for_embeddable/field_selector';
|
import { FieldSelector } from '../../components/log_categorization/log_categorization_for_embeddable/field_selector';
|
||||||
import { SamplingPanel } from '../../components/log_categorization/sampling_menu/sampling_panel';
|
import { SamplingPanel } from '../../components/log_categorization/sampling_menu/sampling_panel';
|
||||||
|
|
||||||
|
@ -272,7 +273,7 @@ export const FormControls: FC<{
|
||||||
setSelectedField(null);
|
setSelectedField(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { dataViewFields, messageField } = getMessageField(dataView);
|
const { dataViewFields, messageField } = getCategorizationDataViewField(dataView);
|
||||||
setFields(dataViewFields);
|
setFields(dataViewFields);
|
||||||
const field = dataViewFields.find((f) => f.name === formInput.fieldName);
|
const field = dataViewFields.find((f) => f.name === formInput.fieldName);
|
||||||
if (formInput.fieldName === undefined) {
|
if (formInput.fieldName === undefined) {
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
"@kbn/apm-utils",
|
"@kbn/apm-utils",
|
||||||
"@kbn/ml-field-stats-flyout",
|
"@kbn/ml-field-stats-flyout",
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
|
"@kbn/aiops-utils",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -38,5 +38,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||||
loadTestFile(require.resolve('./change_point_detection_cases'));
|
loadTestFile(require.resolve('./change_point_detection_cases'));
|
||||||
loadTestFile(require.resolve('./log_pattern_analysis'));
|
loadTestFile(require.resolve('./log_pattern_analysis'));
|
||||||
loadTestFile(require.resolve('./log_pattern_analysis_in_discover'));
|
loadTestFile(require.resolve('./log_pattern_analysis_in_discover'));
|
||||||
|
loadTestFile(require.resolve('./log_pattern_analysis_esql_in_discover'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* 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 expect from '@kbn/expect';
|
||||||
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
|
||||||
|
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
|
const esArchiver = getService('esArchiver');
|
||||||
|
const kibanaServer = getService('kibanaServer');
|
||||||
|
const security = getService('security');
|
||||||
|
const dataGrid = getService('dataGrid');
|
||||||
|
const monacoEditor = getService('monacoEditor');
|
||||||
|
const aiops = getService('aiops');
|
||||||
|
const browser = getService('browser');
|
||||||
|
const retry = getService('retry');
|
||||||
|
const testSubjects = getService('testSubjects');
|
||||||
|
const { common, discover, header, timePicker, unifiedFieldList } = getPageObjects([
|
||||||
|
'common',
|
||||||
|
'discover',
|
||||||
|
'header',
|
||||||
|
'timePicker',
|
||||||
|
'unifiedFieldList',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const defaultSettings = {
|
||||||
|
defaultIndex: 'logstash-*',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function retrySwitchTab(tabIndex: number, seconds: number) {
|
||||||
|
await retry.tryForTime(seconds * 1000, async () => {
|
||||||
|
await browser.switchTab(tabIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('log pattern analysis ES|QL in discover', function () {
|
||||||
|
before(async () => {
|
||||||
|
await kibanaServer.savedObjects.cleanStandardList();
|
||||||
|
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
|
||||||
|
await kibanaServer.importExport.load(
|
||||||
|
'src/platform/test/functional/fixtures/kbn_archiver/discover'
|
||||||
|
);
|
||||||
|
await esArchiver.loadIfNeeded(
|
||||||
|
'src/platform/test/functional/fixtures/es_archiver/logstash_functional'
|
||||||
|
);
|
||||||
|
await kibanaServer.uiSettings.replace(defaultSettings);
|
||||||
|
await common.navigateToApp('discover');
|
||||||
|
await timePicker.setDefaultAbsoluteRange();
|
||||||
|
await discover.waitUntilSearchingHasFinished();
|
||||||
|
await discover.selectTextBaseLang();
|
||||||
|
await discover.waitUntilSearchingHasFinished();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await discover.clickNewSearchButton();
|
||||||
|
await header.waitUntilLoadingHasFinished();
|
||||||
|
await discover.waitUntilSearchingHasFinished();
|
||||||
|
});
|
||||||
|
|
||||||
|
let tabsCount = 1;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (tabsCount > 1) {
|
||||||
|
await browser.closeCurrentWindow();
|
||||||
|
await retrySwitchTab(0, 10);
|
||||||
|
tabsCount--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render categorize fields correctly', async () => {
|
||||||
|
// set query to be categorize command
|
||||||
|
// ensure the columns are correct
|
||||||
|
await monacoEditor.setCodeEditorValue(
|
||||||
|
'from logstash-*\n | STATS Count=COUNT(*) BY Pattern=CATEGORIZE(@message)\n | SORT Count DESC'
|
||||||
|
);
|
||||||
|
await testSubjects.click('querySubmitButton');
|
||||||
|
const columns = ['Count', 'Pattern'];
|
||||||
|
await unifiedFieldList.clickFieldListItemAdd('Count');
|
||||||
|
await unifiedFieldList.clickFieldListItemAdd('Pattern');
|
||||||
|
await header.waitUntilLoadingHasFinished();
|
||||||
|
await discover.waitUntilSearchingHasFinished();
|
||||||
|
expect(await dataGrid.getHeaderFields()).to.eql(columns);
|
||||||
|
|
||||||
|
// refresh the table and ensure the columns are still correct
|
||||||
|
await browser.refresh();
|
||||||
|
await header.waitUntilLoadingHasFinished();
|
||||||
|
await discover.waitUntilSearchingHasFinished();
|
||||||
|
expect(await dataGrid.getHeaderFields()).to.eql(columns);
|
||||||
|
|
||||||
|
// get the first pattern cell and ensure it has the correct number of tokens
|
||||||
|
const cell = await dataGrid.getCellElement(0, 3);
|
||||||
|
const tokens = await cell.findAllByCssSelector('.unifiedDataTable__cellValue code');
|
||||||
|
|
||||||
|
expect(tokens.length).to.be.greaterThan(0);
|
||||||
|
|
||||||
|
// click the expand action button in the cell
|
||||||
|
// ensure the popover is displayed and the regex is correct
|
||||||
|
await dataGrid.clickCellExpandButton(0, { columnIndex: 3, columnName: 'Pattern' });
|
||||||
|
|
||||||
|
const regexElement = await testSubjects.find('euiDataGridExpansionPopover-patternRegex');
|
||||||
|
const regexText = await regexElement.getVisibleText();
|
||||||
|
// The pattern might change based on the data, so rather than checking for an exact match,
|
||||||
|
// we check that the string contains part of a regex
|
||||||
|
expect(regexText).to.contain('.*?');
|
||||||
|
|
||||||
|
// Click the view docs in Discover button
|
||||||
|
// ensure it opens a new tab and the discover doc count is greater than 0
|
||||||
|
// We cannot look for an exact count as the count may vary based on sampling (when that is added)
|
||||||
|
await dataGrid.clickCellExpandPopoverAction('patterns-action-view-docs-in-discover');
|
||||||
|
await retrySwitchTab(1, 10);
|
||||||
|
tabsCount++;
|
||||||
|
|
||||||
|
await aiops.logPatternAnalysisPage.assertDiscoverDocCountExists();
|
||||||
|
// ensure the discover doc count is greater than 0
|
||||||
|
await aiops.logPatternAnalysisPage.assertDiscoverDocCountGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('log pattern analysis', function () {
|
describe('log pattern analysis in discover', function () {
|
||||||
let tabsCount = 1;
|
let tabsCount = 1;
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
|
@ -3849,6 +3849,10 @@
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
||||||
|
"@kbn/aiops-utils@link:src/platform/packages/shared/kbn-aiops-utils":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
"@kbn/alerting-api-integration-helpers@link:x-pack/platform/test/alerting_api_integration/packages/helpers":
|
"@kbn/alerting-api-integration-helpers@link:x-pack/platform/test/alerting_api_integration/packages/helpers":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue