From 096bf5515a34f1158a3bd1c0a259388b337f1f68 Mon Sep 17 00:00:00 2001
From: Nassim Kammah
Date: Thu, 8 Feb 2024 08:42:35 +0100
Subject: [PATCH 001/104] Update docs-preview link (#176468)
## Summary
Following the migration from Jenkins to Buildkite, docs previews are now
available at _bk_.
More context in https://github.com/elastic/docs/pull/2898
### Checklist
### Risk Matrix
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.buildkite/scripts/lifecycle/post_build.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.buildkite/scripts/lifecycle/post_build.sh b/.buildkite/scripts/lifecycle/post_build.sh
index b5bdfd751a6e3..446ca4b28c559 100755
--- a/.buildkite/scripts/lifecycle/post_build.sh
+++ b/.buildkite/scripts/lifecycle/post_build.sh
@@ -12,7 +12,7 @@ fi
ts-node "$(dirname "${0}")/ci_stats_complete.ts"
if [[ "${GITHUB_PR_NUMBER:-}" ]]; then
- DOCS_CHANGES_URL="https://kibana_$GITHUB_PR_NUMBER}.docs-preview.app.elstc.co/diff"
+ DOCS_CHANGES_URL="https://kibana_bk_$GITHUB_PR_NUMBER}.docs-preview.app.elstc.co/diff"
DOCS_CHANGES=$(curl --connect-timeout 10 -m 10 -sf "$DOCS_CHANGES_URL" || echo '')
if [[ "$DOCS_CHANGES" && "$DOCS_CHANGES" != "There aren't any differences!" ]]; then
From e6866fcaf95af3046db55a65f37c4ae7a653cd3d Mon Sep 17 00:00:00 2001
From: Elastic Machine
Date: Thu, 8 Feb 2024 19:30:50 +1030
Subject: [PATCH 002/104] [main] Sync bundled packages with Package Storage
(#176446)
Automated by
https://buildkite.com/elastic/package-storage-infra-kibana-discover-release-branches/builds/332
---
fleet_packages.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fleet_packages.json b/fleet_packages.json
index 79722187a1b0d..836e65d0e95aa 100644
--- a/fleet_packages.json
+++ b/fleet_packages.json
@@ -52,7 +52,7 @@
},
{
"name": "synthetics",
- "version": "1.1.1"
+ "version": "1.2.1"
},
{
"name": "security_detection_engine",
From 8612ddcf8af9d270bac6c011a46a31910b1f0b8b Mon Sep 17 00:00:00 2001
From: Antonio
Date: Thu, 8 Feb 2024 10:09:19 +0100
Subject: [PATCH 003/104] [Cases] Optional custom fields default values
(#176282)
## Summary
Optional default values now support default values.
- The Case Configuration API does not throw anymore if we try defining a
`defaultValue` when `required: false`
- The Case Configuration UI allows defining a default value even if
`required` is not selected.
- The Create Case page populates all custom fields with their default
values. **Even if they are optional.**
- The Case Detail page suggests the default value for **optional text
custom fields** when they are empty.
- For optional `toggle` custom fields the default value is not taken
into account in the Case Detail page.
---
.../components/configure_cases/index.test.tsx | 1 +
.../components/custom_fields/flyout.test.tsx | 22 ++++++
.../custom_fields/form_fields.test.tsx | 1 +
.../custom_fields/text/configure.test.tsx | 21 ++++++
.../custom_fields/text/configure.tsx | 27 +++----
.../custom_fields/toggle/configure.test.tsx | 7 +-
.../custom_fields/toggle/configure.tsx | 28 +++----
.../cases/server/client/cases/utils.test.ts | 6 +-
.../cases/server/client/cases/utils.ts | 6 +-
.../server/client/configure/client.test.ts | 47 ------------
.../cases/server/client/configure/client.ts | 9 +--
.../client/configure/validators.test.ts | 74 +------------------
.../server/client/configure/validators.ts | 34 ---------
.../tests/common/cases/patch_cases.ts | 68 +++++++++++++++++
.../tests/common/cases/post_case.ts | 47 ++++++++++++
.../tests/common/configure/patch_configure.ts | 9 ++-
.../tests/common/configure/post_configure.ts | 17 +++--
17 files changed, 216 insertions(+), 208 deletions(-)
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
index e2db3717c009d..ba3e7850533c9 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
@@ -750,6 +750,7 @@ describe('ConfigureCases', () => {
type: customFieldsConfigurationMock[0].type,
label: `${customFieldsConfigurationMock[0].label}!!`,
required: !customFieldsConfigurationMock[0].required,
+ defaultValue: customFieldsConfigurationMock[0].defaultValue,
},
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
diff --git a/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx
index 3a25009450df7..508f124a7746c 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx
@@ -108,6 +108,27 @@ describe('CustomFieldFlyout ', () => {
});
});
+ it('calls onSaveField with correct params when a custom field is NOT required and has a default value', async () => {
+ appMockRender.render( );
+
+ userEvent.paste(await screen.findByTestId('custom-field-label-input'), 'Summary');
+ userEvent.paste(
+ await screen.findByTestId('text-custom-field-default-value'),
+ 'Default value'
+ );
+ userEvent.click(await screen.findByTestId('custom-field-flyout-save'));
+
+ await waitFor(() => {
+ expect(props.onSaveField).toBeCalledWith({
+ key: expect.anything(),
+ label: 'Summary',
+ required: false,
+ type: CustomFieldTypes.TEXT,
+ defaultValue: 'Default value',
+ });
+ });
+ });
+
it('calls onSaveField with the correct params when a custom field is required', async () => {
appMockRender.render( );
@@ -202,6 +223,7 @@ describe('CustomFieldFlyout ', () => {
label: 'Summary',
required: false,
type: CustomFieldTypes.TOGGLE,
+ defaultValue: false,
});
});
});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx
index 6c392a1ee7d7d..51f5f6dbddea6 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx
@@ -65,6 +65,7 @@ describe('FormFields ', () => {
{
label: 'hello',
type: CustomFieldTypes.TOGGLE,
+ defaultValue: false,
},
true
);
diff --git a/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx
index 5d9d7166db270..455163f225a2b 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx
@@ -44,6 +44,27 @@ describe('Configure ', () => {
});
});
+ it('updates field options with default value correctly when not required', async () => {
+ render(
+
+
+
+ );
+
+ userEvent.paste(await screen.findByTestId('text-custom-field-default-value'), 'Default value');
+ userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
+
+ await waitFor(() => {
+ // data, isValid
+ expect(onSubmit).toBeCalledWith(
+ {
+ defaultValue: 'Default value',
+ },
+ true
+ );
+ });
+ });
+
it('updates field options correctly when required', async () => {
render(
diff --git a/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx b/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx
index 1253640d91b79..2ec61a4b80529 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { UseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { CheckBoxField, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import type { CaseCustomFieldText } from '../../../../common/types/domain';
import type { CustomFieldType } from '../types';
@@ -14,7 +14,6 @@ import { getTextFieldConfig } from './config';
import * as i18n from '../translations';
const ConfigureComponent: CustomFieldType['Configure'] = () => {
- const [{ required }] = useFormData<{ required: boolean }>();
const config = getTextFieldConfig({
required: false,
label: i18n.DEFAULT_VALUE.toLocaleLowerCase(),
@@ -34,19 +33,17 @@ const ConfigureComponent: CustomFieldType['Configure'] = ()
},
}}
/>
- {required && (
-
- )}
+
>
);
};
diff --git a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx
index 8153ca64a789c..77f1bde4e7b55 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx
@@ -41,7 +41,12 @@ describe('Configure ', () => {
await waitFor(() => {
// data, isValid
- expect(onSubmit).toBeCalledWith({}, true);
+ expect(onSubmit).toBeCalledWith(
+ {
+ defaultValue: false,
+ },
+ true
+ );
});
});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx
index 83645ae185f1d..7b5980c2276ec 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx
@@ -6,15 +6,13 @@
*/
import React from 'react';
-import { UseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { CheckBoxField, ToggleField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import type { CaseCustomFieldToggle } from '../../../../common/types/domain';
import type { CustomFieldType } from '../types';
import * as i18n from '../translations';
const ConfigureComponent: CustomFieldType['Configure'] = () => {
- const [{ required }] = useFormData<{ required: boolean }>();
-
return (
<>
['Configure'] =
},
}}
/>
- {required && (
-
- )}
+
>
);
};
diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts
index a1c070eb08d1f..c6c9f1063df60 100644
--- a/x-pack/plugins/cases/server/client/cases/utils.test.ts
+++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts
@@ -1418,7 +1418,7 @@ describe('utils', () => {
).toEqual(customFields);
});
- it('does not use default value for optional custom fields', () => {
+ it('uses the default value for optional custom fields', () => {
expect(
fillMissingCustomFields({
customFields: [],
@@ -1430,8 +1430,8 @@ describe('utils', () => {
],
})
).toEqual([
- { ...customFields[0], value: null },
- { ...customFields[1], value: null },
+ { ...customFields[0], value: 'default value' },
+ { ...customFields[1], value: true },
]);
});
diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts
index b304159b928aa..a7f596a0f9c9e 100644
--- a/x-pack/plugins/cases/server/client/cases/utils.ts
+++ b/x-pack/plugins/cases/server/client/cases/utils.ts
@@ -473,11 +473,7 @@ export const fillMissingCustomFields = ({
// only populate with the default value required custom fields missing from the request
for (const confCustomField of customFieldsConfiguration) {
if (!customFieldsKeys.has(confCustomField.key)) {
- if (
- confCustomField.required &&
- confCustomField?.defaultValue !== null &&
- confCustomField?.defaultValue !== undefined
- ) {
+ if (confCustomField?.defaultValue !== null && confCustomField?.defaultValue !== undefined) {
missingCustomFields.push({
key: confCustomField.key,
type: confCustomField.type,
diff --git a/x-pack/plugins/cases/server/client/configure/client.test.ts b/x-pack/plugins/cases/server/client/configure/client.test.ts
index 2e3c2a6899f91..b5958c44de080 100644
--- a/x-pack/plugins/cases/server/client/configure/client.test.ts
+++ b/x-pack/plugins/cases/server/client/configure/client.test.ts
@@ -346,30 +346,6 @@ describe('client', () => {
'Failed to get patch configure in route: Error: Invalid custom field types in request for the following labels: "text label"'
);
});
-
- it('throws when an optional custom field has a default value', async () => {
- await expect(
- update(
- 'test-id',
- {
- version: 'test-version',
- customFields: [
- {
- key: 'extra_default',
- label: 'text label',
- type: CustomFieldTypes.TEXT,
- required: false,
- defaultValue: 'foobar',
- },
- ],
- },
- clientArgs,
- casesClientInternal
- )
- ).rejects.toThrow(
- 'Failed to get patch configure in route: Error: The following optional custom fields try to define a default value: "text label"'
- );
- });
});
describe('create', () => {
@@ -431,28 +407,5 @@ describe('client', () => {
'Failed to create case configuration: Error: Invalid duplicated custom field keys in request: duplicated_key'
);
});
-
- it('throws when an optional custom field has a default value', async () => {
- await expect(
- create(
- {
- ...baseRequest,
- customFields: [
- {
- key: 'extra_default',
- label: 'text label',
- type: CustomFieldTypes.TEXT,
- required: false,
- defaultValue: 'foobar',
- },
- ],
- },
- clientArgs,
- casesClientInternal
- )
- ).rejects.toThrow(
- 'Failed to create case configuration: Error: The following optional custom fields try to define a default value: "text label"'
- );
- });
});
});
diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts
index 41a9dd9326c24..1261f1061a371 100644
--- a/x-pack/plugins/cases/server/client/configure/client.ts
+++ b/x-pack/plugins/cases/server/client/configure/client.ts
@@ -49,10 +49,7 @@ import { updateMappings } from './update_mappings';
import { decodeOrThrow } from '../../../common/api/runtime_types';
import { ConfigurationRt, ConfigurationsRt } from '../../../common/types/domain';
import { validateDuplicatedCustomFieldKeysInRequest } from '../validators';
-import {
- validateCustomFieldTypesInRequest,
- validateOptionalCustomFieldsInRequest,
-} from './validators';
+import { validateCustomFieldTypesInRequest } from './validators';
/**
* Defines the internal helper functions.
@@ -256,7 +253,6 @@ export async function update(
const request = decodeWithExcessOrThrow(ConfigurationPatchRequestRt)(req);
validateDuplicatedCustomFieldKeysInRequest({ requestCustomFields: request.customFields });
- validateOptionalCustomFieldsInRequest({ requestCustomFields: request.customFields });
const { version, ...queryWithoutVersion } = request;
@@ -372,9 +368,6 @@ export async function create(
validateDuplicatedCustomFieldKeysInRequest({
requestCustomFields: validatedConfigurationRequest.customFields,
});
- validateOptionalCustomFieldsInRequest({
- requestCustomFields: validatedConfigurationRequest.customFields,
- });
let error = null;
diff --git a/x-pack/plugins/cases/server/client/configure/validators.test.ts b/x-pack/plugins/cases/server/client/configure/validators.test.ts
index d1d41bfe1bb62..0f8e20505fb39 100644
--- a/x-pack/plugins/cases/server/client/configure/validators.test.ts
+++ b/x-pack/plugins/cases/server/client/configure/validators.test.ts
@@ -6,10 +6,7 @@
*/
import { CustomFieldTypes } from '../../../common/types/domain';
-import {
- validateCustomFieldTypesInRequest,
- validateOptionalCustomFieldsInRequest,
-} from './validators';
+import { validateCustomFieldTypesInRequest } from './validators';
describe('validators', () => {
describe('validateCustomFieldTypesInRequest', () => {
@@ -72,73 +69,4 @@ describe('validators', () => {
).not.toThrow();
});
});
-
- describe('validateOptionalCustomFieldsInRequest', () => {
- it('does not throw an error for properly constructed optional custom fields', () => {
- expect(() =>
- validateOptionalCustomFieldsInRequest({
- requestCustomFields: [
- { key: '1', required: false, label: 'label 1' },
- { key: '2', required: false, label: 'label 2' },
- ],
- })
- ).not.toThrow();
- });
-
- it('does not throw an error for required custom fields with default values', () => {
- expect(() =>
- validateOptionalCustomFieldsInRequest({
- requestCustomFields: [
- { key: '1', required: true, defaultValue: false, label: 'label 1' },
- { key: '2', required: true, defaultValue: 'foobar', label: 'label 2' },
- ],
- })
- ).not.toThrow();
- });
-
- it('throws an error even if the default value has the correct type', () => {
- expect(() =>
- validateOptionalCustomFieldsInRequest({
- requestCustomFields: [
- { key: '1', required: false, defaultValue: false, label: 'label 1' },
- { key: '2', required: false, defaultValue: 'foobar', label: 'label 2' },
- ],
- })
- ).toThrowErrorMatchingInlineSnapshot(
- `"The following optional custom fields try to define a default value: \\"label 1\\", \\"label 2\\""`
- );
- });
-
- it('throws an error for other falsy defaultValues (null)', () => {
- expect(() =>
- validateOptionalCustomFieldsInRequest({
- requestCustomFields: [
- { key: '1', required: false, defaultValue: null, label: 'label 1' },
- ],
- })
- ).toThrowErrorMatchingInlineSnapshot(
- `"The following optional custom fields try to define a default value: \\"label 1\\""`
- );
- });
-
- it('throws an error for other falsy defaultValues (0)', () => {
- expect(() =>
- validateOptionalCustomFieldsInRequest({
- requestCustomFields: [{ key: '1', required: false, defaultValue: 0, label: 'label 1' }],
- })
- ).toThrowErrorMatchingInlineSnapshot(
- `"The following optional custom fields try to define a default value: \\"label 1\\""`
- );
- });
-
- it('throws an error for other falsy defaultValues (empty string)', () => {
- expect(() =>
- validateOptionalCustomFieldsInRequest({
- requestCustomFields: [{ key: '1', required: false, defaultValue: '', label: 'label 1' }],
- })
- ).toThrowErrorMatchingInlineSnapshot(
- `"The following optional custom fields try to define a default value: \\"label 1\\""`
- );
- });
- });
});
diff --git a/x-pack/plugins/cases/server/client/configure/validators.ts b/x-pack/plugins/cases/server/client/configure/validators.ts
index ca3a175e40579..c5929065c631b 100644
--- a/x-pack/plugins/cases/server/client/configure/validators.ts
+++ b/x-pack/plugins/cases/server/client/configure/validators.ts
@@ -38,37 +38,3 @@ export const validateCustomFieldTypesInRequest = ({
);
}
};
-
-/**
- * Throws an error if any optional custom field defines a default value.
- */
-export const validateOptionalCustomFieldsInRequest = ({
- requestCustomFields,
-}: {
- requestCustomFields?: Array<{
- key: string;
- required: boolean;
- defaultValue?: unknown;
- label: string;
- }>;
-}) => {
- if (!Array.isArray(requestCustomFields)) {
- return;
- }
-
- const invalidFields: string[] = [];
-
- requestCustomFields.forEach((requestField) => {
- if (!requestField.required && requestField.defaultValue !== undefined) {
- invalidFields.push(`"${requestField.label}"`);
- }
- });
-
- if (invalidFields.length > 0) {
- throw Boom.badRequest(
- `The following optional custom fields try to define a default value: ${invalidFields.join(
- ', '
- )}`
- );
- }
-};
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts
index cc49ddf44bdcc..3a965b73004ef 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts
@@ -1149,6 +1149,74 @@ export default ({ getService }: FtrProviderContext): void => {
]);
});
+ it('patches a case with missing optional custom fields to their default values', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'text_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ defaultValue: 'default value',
+ required: false,
+ },
+ {
+ key: 'toggle_custom_field',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ defaultValue: false,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+
+ const originalValues = [
+ {
+ key: 'text_custom_field',
+ type: CustomFieldTypes.TEXT,
+ value: 'hello',
+ },
+ {
+ key: 'toggle_custom_field',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ] as CaseCustomFields;
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: originalValues,
+ });
+
+ const patchedCases = await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: postedCase.id,
+ version: postedCase.version,
+ customFields: [
+ {
+ key: 'toggle_custom_field',
+ type: CustomFieldTypes.TOGGLE,
+ value: false,
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ expect(patchedCases[0].customFields).to.eql([
+ { ...originalValues[1], value: false },
+ { ...originalValues[0], value: 'default value' },
+ ]);
+ });
+
it('400s trying to patch a case with missing required custom fields if they dont have default values', async () => {
await createConfiguration(
supertest,
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts
index 5049c04b060a8..411932212f242 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts
@@ -318,6 +318,53 @@ export default ({ getService }: FtrProviderContext): void => {
},
]);
});
+
+ it('creates a case with missing optional custom fields and default values', async () => {
+ const customFieldsConfiguration = [
+ {
+ key: 'text_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ defaultValue: 'default value',
+ },
+ {
+ key: 'toggle_custom_field',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ defaultValue: false,
+ required: false,
+ },
+ ];
+
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: customFieldsConfiguration,
+ },
+ })
+ );
+ const createdCase = await createCase(
+ supertest,
+ getPostCaseRequest({
+ customFields: [],
+ })
+ );
+
+ expect(createdCase.customFields).to.eql([
+ {
+ key: customFieldsConfiguration[0].key,
+ type: customFieldsConfiguration[0].type,
+ value: 'default value',
+ },
+ {
+ key: customFieldsConfiguration[1].key,
+ type: customFieldsConfiguration[1].type,
+ value: false,
+ },
+ ]);
+ });
});
describe('unhappy path', () => {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts
index d5b6e931ca671..c8e0f092edf3a 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts
@@ -64,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => {
required: true,
},
{
- key: 'toggle_field',
+ key: 'toggle_field_1',
label: '#2',
type: CustomFieldTypes.TOGGLE,
required: false,
@@ -76,6 +76,13 @@ export default ({ getService }: FtrProviderContext): void => {
required: true,
defaultValue: 'foobar',
},
+ {
+ key: 'toggle_field_2',
+ label: '#4',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ defaultValue: true,
+ },
] as ConfigurationPatchRequest['customFields'];
const configuration = await createConfiguration(supertest);
const newConfiguration = await updateConfiguration(supertest, configuration.id, {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts
index 15efb00444993..a7461d5f1fc18 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts
@@ -63,20 +63,27 @@ export default ({ getService }: FtrProviderContext): void => {
it('should create a configuration with customFields', async () => {
const customFields = {
customFields: [
- { key: 'hello', label: 'text', type: CustomFieldTypes.TEXT, required: false },
+ { key: 'text_1', label: 'text 1', type: CustomFieldTypes.TEXT, required: false },
{
- key: 'goodbye',
- label: 'toggle',
+ key: 'toggle_1',
+ label: 'toggle 1',
type: CustomFieldTypes.TOGGLE,
required: true,
defaultValue: false,
},
{
- key: 'hello_again',
- label: 'text',
+ key: 'text_2',
+ label: 'text 2',
type: CustomFieldTypes.TEXT,
required: true,
},
+ {
+ key: 'toggle_2',
+ label: 'toggle 2',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ defaultValue: true,
+ },
],
};
From 0bca3c0ecfb0da135d1799ed6f956436b9ea27f6 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Thu, 8 Feb 2024 09:22:35 +0000
Subject: [PATCH 004/104] [ML] Adds grok highlighting to the file data
visualizer (#175913)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds grokpattern highlighting to the file data visualizer for
semi-structured text files.
The first 5 lines of the file are displayed with inline highlighting.
Hovering the mouse over displays a tooltip with the field name and type.
![image](https://github.com/elastic/kibana/assets/22172091/7b50aeca-0255-4413-93ef-e44976e798f4)
If for whatever reason the highlighting fails, we switch back to the raw
text.
@szabosteve and @peteharverson I'm not 100% happy with the labels on the
tabs, `Highlighted text` and `Raw text`. So suggestions are welcome.
Relates to https://github.com/elastic/elasticsearch/pull/104394
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: István Zoltán Szabó
---
.../common/types/test_grok_pattern.ts | 13 ++
.../components/file_contents/field_badge.tsx | 62 ++++++++
.../file_contents/file_contents.tsx | 150 +++++++++++++++---
.../file_contents/grok_highlighter.ts | 103 ++++++++++++
.../file_contents/use_text_parser.tsx | 67 ++++++++
.../components/results_view/results_view.tsx | 13 ++
.../plugins/data_visualizer/server/index.ts | 2 +-
.../plugins/data_visualizer/server/plugin.ts | 18 ++-
.../plugins/data_visualizer/server/routes.ts | 69 ++++++++
x-pack/plugins/data_visualizer/tsconfig.json | 35 ++--
.../translations/translations/fr-FR.json | 1 -
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../data_visualizer/file_data_visualizer.ts | 13 +-
.../services/ml/data_visualizer_file_based.ts | 26 +++
15 files changed, 521 insertions(+), 53 deletions(-)
create mode 100644 x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts
create mode 100644 x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx
create mode 100644 x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts
create mode 100644 x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx
create mode 100644 x-pack/plugins/data_visualizer/server/routes.ts
diff --git a/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts b/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts
new file mode 100644
index 0000000000000..65ae4a89988de
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface TestGrokPatternResponse {
+ matches: Array<{
+ matched: boolean;
+ fields: Record>;
+ }>;
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx
new file mode 100644
index 0000000000000..981b2195c3065
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
+import { FieldIcon } from '@kbn/react-field';
+import { i18n } from '@kbn/i18n';
+import { getSupportedFieldType } from '../../../common/components/fields_stats_grid/get_field_names';
+import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
+
+interface Props {
+ type: string | undefined;
+ value: string;
+ name: string;
+}
+
+export const FieldBadge: FC = ({ type, value, name }) => {
+ const { euiColorLightestShade, euiColorLightShade } = useCurrentEuiTheme();
+ const supportedType = getSupportedFieldType(type ?? 'unknown');
+ const tooltip = type
+ ? i18n.translate('xpack.dataVisualizer.file.fileContents.fieldBadge.tooltip', {
+ defaultMessage: 'Type: {type}',
+ values: { type: supportedType },
+ })
+ : undefined;
+ return (
+
+
+
+
+
+
+ {value}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx
index 21c50a7f293b6..789d73888bf35 100644
--- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx
@@ -5,52 +5,150 @@
* 2.0.
*/
+import React, { FC, useEffect, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import React, { FC } from 'react';
-import { EuiTitle, EuiSpacer } from '@elastic/eui';
+import {
+ EuiTitle,
+ EuiSpacer,
+ EuiHorizontalRule,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSwitch,
+} from '@elastic/eui';
-import { JsonEditor, EDITOR_MODE } from '../json_editor';
+import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
+import useMountedState from 'react-use/lib/useMountedState';
+import { i18n } from '@kbn/i18n';
+import { EDITOR_MODE, JsonEditor } from '../json_editor';
+import { useGrokHighlighter } from './use_text_parser';
+import { LINE_LIMIT } from './grok_highlighter';
interface Props {
data: string;
format: string;
numberOfLines: number;
+ semiStructureTextData: SemiStructureTextData | null;
}
-export const FileContents: FC = ({ data, format, numberOfLines }) => {
+interface SemiStructureTextData {
+ grokPattern?: string;
+ multilineStartPattern?: string;
+ excludeLinesPattern?: string;
+ sampleStart: string;
+ mappings: FindFileStructureResponse['mappings'];
+ ecsCompatibility?: string;
+}
+
+function semiStructureTextDataGuard(
+ semiStructureTextData: SemiStructureTextData | null
+): semiStructureTextData is SemiStructureTextData {
+ return (
+ semiStructureTextData !== null &&
+ semiStructureTextData.grokPattern !== undefined &&
+ semiStructureTextData.multilineStartPattern !== undefined
+ );
+}
+
+export const FileContents: FC = ({ data, format, numberOfLines, semiStructureTextData }) => {
let mode = EDITOR_MODE.TEXT;
if (format === EDITOR_MODE.JSON) {
mode = EDITOR_MODE.JSON;
}
+ const isMounted = useMountedState();
+ const grokHighlighter = useGrokHighlighter();
+
+ const [isSemiStructureTextData, setIsSemiStructureTextData] = useState(
+ semiStructureTextDataGuard(semiStructureTextData)
+ );
+ const formattedData = useMemo(
+ () => limitByNumberOfLines(data, numberOfLines),
+ [data, numberOfLines]
+ );
+
+ const [highlightedLines, setHighlightedLines] = useState(null);
+ const [showHighlights, setShowHighlights] = useState(isSemiStructureTextData);
+
+ useEffect(() => {
+ if (isSemiStructureTextData === false) {
+ return;
+ }
+ const { grokPattern, multilineStartPattern, excludeLinesPattern, mappings, ecsCompatibility } =
+ semiStructureTextData!;
- const formattedData = limitByNumberOfLines(data, numberOfLines);
+ grokHighlighter(
+ data,
+ grokPattern!,
+ mappings,
+ ecsCompatibility,
+ multilineStartPattern!,
+ excludeLinesPattern
+ )
+ .then((docs) => {
+ if (isMounted()) {
+ setHighlightedLines(docs);
+ }
+ })
+ .catch((e) => {
+ if (isMounted()) {
+ setHighlightedLines(null);
+ setIsSemiStructureTextData(false);
+ }
+ });
+ }, [data, semiStructureTextData, grokHighlighter, isSemiStructureTextData, isMounted]);
return (
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+ {isSemiStructureTextData ? (
+
+ setShowHighlights(!showHighlights)}
+ />
+
+ ) : null}
+
+
+
+
+
-
-
+ {highlightedLines === null || showHighlights === false ? (
+
+ ) : (
+ <>
+ {highlightedLines.map((line, i) => (
+ <>
+ {line}
+ {i === highlightedLines.length - 1 ? null : }
+ >
+ ))}
+ >
+ )}
+ >
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts
new file mode 100644
index 0000000000000..7be566a5a91b0
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { MessageImporter } from '@kbn/file-upload-plugin/public';
+import type { HttpSetup } from '@kbn/core/public';
+import type { ImportFactoryOptions } from '@kbn/file-upload-plugin/public/importer';
+import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
+import type { TestGrokPatternResponse } from '../../../../../common/types/test_grok_pattern';
+
+export const LINE_LIMIT = 5;
+
+type HighlightedLine = Array<{
+ word: string;
+ field?: {
+ type: string;
+ name: string;
+ };
+}>;
+
+export class GrokHighlighter extends MessageImporter {
+ constructor(options: ImportFactoryOptions, private http: HttpSetup) {
+ super(options);
+ }
+
+ public async createLines(
+ text: string,
+ grokPattern: string,
+ mappings: FindFileStructureResponse['mappings'],
+ ecsCompatibility: string | undefined
+ ): Promise {
+ const docs = this._createDocs(text, false, LINE_LIMIT);
+ const lines = docs.docs.map((doc) => doc.message);
+ const matches = await this.testGrokPattern(lines, grokPattern, ecsCompatibility);
+
+ return lines.map((line, index) => {
+ const { matched, fields } = matches[index];
+ if (matched === false) {
+ return [
+ {
+ word: line,
+ },
+ ];
+ }
+ const sortedFields = Object.entries(fields)
+ .map(([fieldName, [{ match, offset, length }]]) => {
+ let type = mappings.properties[fieldName]?.type;
+ if (type === undefined && fieldName === 'timestamp') {
+ // it's possible that the timestamp field is not mapped as `timestamp`
+ // but instead as `@timestamp`
+ type = mappings.properties['@timestamp']?.type;
+ }
+ return {
+ name: fieldName,
+ match,
+ offset,
+ length,
+ type,
+ };
+ })
+ .sort((a, b) => a.offset - b.offset);
+
+ let offset = 0;
+ const highlightedLine: HighlightedLine = [];
+ for (const field of sortedFields) {
+ highlightedLine.push({ word: line.substring(offset, field.offset) });
+ highlightedLine.push({
+ word: field.match,
+ field: {
+ type: field.type,
+ name: field.name,
+ },
+ });
+ offset = field.offset + field.length;
+ }
+ highlightedLine.push({ word: line.substring(offset) });
+ return highlightedLine;
+ });
+ }
+
+ private async testGrokPattern(
+ lines: string[],
+ grokPattern: string,
+ ecsCompatibility: string | undefined
+ ) {
+ const { matches } = await this.http.fetch(
+ '/internal/data_visualizer/test_grok_pattern',
+ {
+ method: 'POST',
+ version: '1',
+ body: JSON.stringify({
+ grokPattern,
+ text: lines,
+ ecsCompatibility,
+ }),
+ }
+ );
+ return matches;
+ }
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx
new file mode 100644
index 0000000000000..183f3ca727d3a
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import { EuiText } from '@elastic/eui';
+import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
+import { FieldBadge } from './field_badge';
+import { useDataVisualizerKibana } from '../../../kibana_context';
+import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
+import { GrokHighlighter } from './grok_highlighter';
+
+export function useGrokHighlighter() {
+ const {
+ services: { http },
+ } = useDataVisualizerKibana();
+ const { euiSizeL } = useCurrentEuiTheme();
+
+ const createLines = useMemo(
+ () =>
+ async (
+ text: string,
+ grokPattern: string,
+ mappings: FindFileStructureResponse['mappings'],
+ ecsCompatibility: string | undefined,
+ multilineStartPattern: string,
+ excludeLinesPattern: string | undefined
+ ) => {
+ const grokHighlighter = new GrokHighlighter(
+ { multilineStartPattern, excludeLinesPattern },
+ http
+ );
+ const lines = await grokHighlighter.createLines(
+ text,
+ grokPattern,
+ mappings,
+ ecsCompatibility
+ );
+
+ return lines.map((line) => {
+ const formattedWords: JSX.Element[] = [];
+ for (const { word, field } of line) {
+ if (field) {
+ formattedWords.push( );
+ } else {
+ formattedWords.push({word} );
+ }
+ }
+ return (
+
+ {formattedWords}
+
+ );
+ });
+ },
+ [euiSizeL, http]
+ );
+
+ return createLines;
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx
index 26a727a7a922e..855df5855536a 100644
--- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx
@@ -20,6 +20,7 @@ import {
} from '@elastic/eui';
import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
+import { FILE_FORMATS } from '../../../../../common/constants';
import { FileContents } from '../file_contents';
import { AnalysisSummary } from '../analysis_summary';
import { FieldsStatsGrid } from '../../../common/components/fields_stats_grid';
@@ -48,6 +49,17 @@ export const ResultsView: FC = ({
onCancel,
disableImport,
}) => {
+ const semiStructureTextData =
+ results.format === FILE_FORMATS.SEMI_STRUCTURED_TEXT
+ ? {
+ grokPattern: results.grok_pattern,
+ multilineStartPattern: results.multiline_start_pattern,
+ sampleStart: results.sample_start,
+ excludeLinesPattern: results.exclude_lines_pattern,
+ mappings: results.mappings,
+ ecsCompatibility: results.ecs_compatibility,
+ }
+ : null;
return (
@@ -77,6 +89,7 @@ export const ResultsView: FC = ({
data={data}
format={results.format}
numberOfLines={results.num_lines_analyzed}
+ semiStructureTextData={semiStructureTextData}
/>
diff --git a/x-pack/plugins/data_visualizer/server/index.ts b/x-pack/plugins/data_visualizer/server/index.ts
index 1f15b498f8777..17db3c1abb603 100644
--- a/x-pack/plugins/data_visualizer/server/index.ts
+++ b/x-pack/plugins/data_visualizer/server/index.ts
@@ -9,5 +9,5 @@ import { PluginInitializerContext } from '@kbn/core/server';
export const plugin = async (initializerContext: PluginInitializerContext) => {
const { DataVisualizerPlugin } = await import('./plugin');
- return new DataVisualizerPlugin();
+ return new DataVisualizerPlugin(initializerContext);
};
diff --git a/x-pack/plugins/data_visualizer/server/plugin.ts b/x-pack/plugins/data_visualizer/server/plugin.ts
index 5f16df8b5ffb7..0e70f756f9b21 100644
--- a/x-pack/plugins/data_visualizer/server/plugin.ts
+++ b/x-pack/plugins/data_visualizer/server/plugin.ts
@@ -5,18 +5,30 @@
* 2.0.
*/
-import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
-import { StartDeps, SetupDeps } from './types';
+import type {
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ Logger,
+ PluginInitializerContext,
+} from '@kbn/core/server';
+import type { StartDeps, SetupDeps } from './types';
import { registerWithCustomIntegrations } from './register_custom_integration';
+import { routes } from './routes';
export class DataVisualizerPlugin implements Plugin {
- constructor() {}
+ private readonly _logger: Logger;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this._logger = initializerContext.logger.get();
+ }
setup(coreSetup: CoreSetup, plugins: SetupDeps) {
// home-plugin required
if (plugins.home && plugins.customIntegrations) {
registerWithCustomIntegrations(plugins.customIntegrations);
}
+ routes(coreSetup, this._logger);
}
start(core: CoreStart) {}
diff --git a/x-pack/plugins/data_visualizer/server/routes.ts b/x-pack/plugins/data_visualizer/server/routes.ts
new file mode 100644
index 0000000000000..c4e286f9671d1
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/server/routes.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { CoreSetup, Logger } from '@kbn/core/server';
+import { schema } from '@kbn/config-schema';
+import type { StartDeps } from './types';
+import { wrapError } from './utils/error_wrapper';
+import type { TestGrokPatternResponse } from '../common/types/test_grok_pattern';
+
+/**
+ * @apiGroup DataVisualizer
+ *
+ * @api {post} /internal/data_visualizer/test_grok_pattern Tests a grok pattern against a sample of text
+ * @apiName testGrokPattern
+ * @apiDescription Tests a grok pattern against a sample of text and return the positions of the fields
+ */
+export function routes(coreSetup: CoreSetup, logger: Logger) {
+ const router = coreSetup.http.createRouter();
+
+ router.versioned
+ .post({
+ path: '/internal/data_visualizer/test_grok_pattern',
+ access: 'internal',
+ options: {
+ tags: ['access:fileUpload:analyzeFile'],
+ },
+ })
+ .addVersion(
+ {
+ version: '1',
+ validate: {
+ request: {
+ body: schema.object({
+ grokPattern: schema.string(),
+ text: schema.arrayOf(schema.string()),
+ ecsCompatibility: schema.maybe(schema.string()),
+ }),
+ },
+ },
+ },
+ async (context, request, response) => {
+ try {
+ const esClient = (await context.core).elasticsearch.client;
+ const body = await esClient.asInternalUser.transport.request({
+ method: 'GET',
+ path: `/_text_structure/test_grok_pattern`,
+ body: {
+ grok_pattern: request.body.grokPattern,
+ text: request.body.text,
+ },
+ ...(request.body.ecsCompatibility
+ ? {
+ querystring: { ecs_compatibility: request.body.ecsCompatibility },
+ }
+ : {}),
+ });
+
+ return response.ok({ body });
+ } catch (e) {
+ logger.warn(`Unable to test grok pattern ${e.message}`);
+ return response.customError(wrapError(e));
+ }
+ }
+ );
+}
diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json
index af962ac08b6e8..aad960d856da3 100644
--- a/x-pack/plugins/data_visualizer/tsconfig.json
+++ b/x-pack/plugins/data_visualizer/tsconfig.json
@@ -17,20 +17,28 @@
"@kbn/aiops-utils",
"@kbn/charts-plugin",
"@kbn/cloud-plugin",
+ "@kbn/code-editor",
+ "@kbn/config-schema",
"@kbn/core-execution-context-common",
+ "@kbn/core-notifications-browser",
"@kbn/core",
"@kbn/custom-integrations-plugin",
"@kbn/data-plugin",
+ "@kbn/data-service",
"@kbn/data-view-field-editor-plugin",
"@kbn/data-views-plugin",
"@kbn/datemath",
"@kbn/discover-plugin",
+ "@kbn/ebt-tools",
"@kbn/embeddable-plugin",
"@kbn/embeddable-plugin",
"@kbn/es-query",
+ "@kbn/es-types",
"@kbn/es-ui-shared-plugin",
+ "@kbn/esql-utils",
"@kbn/field-formats-plugin",
"@kbn/field-types",
+ "@kbn/field-utils",
"@kbn/file-upload-plugin",
"@kbn/home-plugin",
"@kbn/i18n-react",
@@ -41,41 +49,34 @@
"@kbn/maps-plugin",
"@kbn/ml-agg-utils",
"@kbn/ml-cancellable-search",
+ "@kbn/ml-chi2test",
+ "@kbn/ml-data-grid",
"@kbn/ml-date-picker",
+ "@kbn/ml-error-utils",
+ "@kbn/ml-in-memory-table",
"@kbn/ml-is-defined",
"@kbn/ml-is-populated-object",
+ "@kbn/ml-kibana-theme",
"@kbn/ml-local-storage",
"@kbn/ml-nested-property",
"@kbn/ml-number-utils",
"@kbn/ml-query-utils",
+ "@kbn/ml-random-sampler-utils",
+ "@kbn/ml-string-hash",
"@kbn/ml-url-state",
- "@kbn/ml-data-grid",
- "@kbn/ml-error-utils",
- "@kbn/ml-kibana-theme",
- "@kbn/ml-in-memory-table",
"@kbn/react-field",
"@kbn/rison",
"@kbn/saved-search-plugin",
"@kbn/security-plugin",
"@kbn/share-plugin",
"@kbn/test-jest-helpers",
+ "@kbn/text-based-languages",
"@kbn/ui-actions-plugin",
+ "@kbn/ui-theme",
"@kbn/unified-search-plugin",
"@kbn/usage-collection-plugin",
"@kbn/utility-types",
- "@kbn/ml-string-hash",
- "@kbn/ml-random-sampler-utils",
- "@kbn/data-service",
- "@kbn/core-notifications-browser",
- "@kbn/ebt-tools",
- "@kbn/ml-chi2test",
- "@kbn/field-utils",
- "@kbn/visualization-utils",
- "@kbn/text-based-languages",
- "@kbn/code-editor",
- "@kbn/es-types",
- "@kbn/ui-theme",
- "@kbn/esql-utils"
+ "@kbn/visualization-utils"
],
"exclude": [
"target/**/*",
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index c812c47f9aa48..b8d9c9eb44c43 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -12174,7 +12174,6 @@
"xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{length, plural, one { {lg} } many { Le groupe de lettres {lg} } other { Le groupe de lettres {lg} }} en {format} n'est pas compatible, car il n'est pas précédé de ss ni d'un séparateur de {sep}",
"xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{length, plural, one { {lg} } many { Le groupe de lettres {lg} } other { Le groupe de lettres {lg} }} en {format} n'est pas compatible",
"xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "Le format d'horodatage {timestampFormat} n'est pas compatible, car il contient un point d'interrogation ({fieldPlaceholder})",
- "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "{numberOfLines, plural, one {# ligne} many {# lignes} other {# lignes}} premier(s)",
"xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "La taille du fichier que vous avez sélectionné pour le chargement dépasse la taille maximale autorisée de {maxFileSizeFormatted} de {diffFormatted}",
"xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "La taille du fichier que vous avez sélectionné pour le chargement est de {fileSizeFormatted}, ce qui dépasse la taille maximale autorisée de {maxFileSizeFormatted}",
"xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "Impossible d'importer {importFailuresLength} document(s) sur {docCount}. Cela peut être dû au manque de correspondance entre les lignes et le modèle Grok.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 10bbca87a9cad..cf3ea4c3fe0a9 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -12187,7 +12187,6 @@
"xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{format}の文字{length, plural, other { グループ{lg} }}は、前にssと{sep}の区切り文字が付いていないため、サポートされません",
"xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{format}の文字{length, plural, other { グループ{lg} }}はサポートされていません",
"xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "タイムスタンプフォーマット {timestampFormat} は、疑問符({fieldPlaceholder})が含まれているためサポートされていません",
- "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "最初の{numberOfLines, plural, other {#行}}件",
"xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "アップロードするよう選択されたファイルのサイズが {diffFormatted} に許可された最大サイズの {maxFileSizeFormatted} を超えています",
"xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "アップロードするよう選択されたファイルのサイズは {fileSizeFormatted} で、許可された最大サイズの {maxFileSizeFormatted} を超えています",
"xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "{docCount}件中{importFailuresLength}件のドキュメントをインポートできません。行が Grok パターンと一致していないことが原因の可能性があります。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index bd37c0d9f3fb6..26029615545f8 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -12281,7 +12281,6 @@
"xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{format}的字母 {length, plural, other { 组 {lg} }} 不受支持,因为其未前置 ss 和 {sep} 中的分隔符",
"xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{format}的字母 {length, plural, other { 组 {lg} }} 不受支持",
"xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "时间戳格式 {timestampFormat} 不受支持,因为其包含问号字符 ({fieldPlaceholder})",
- "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "前 {numberOfLines, plural, other {# 行}}",
"xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "您选择用于上传的文件大小超过上限值 {maxFileSizeFormatted} 的 {diffFormatted}",
"xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "您选择用于上传的文件大小为 {fileSizeFormatted},超过上限值 {maxFileSizeFormatted}",
"xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "无法导入 {importFailuresLength} 个文档(共 {docCount} 个)。这可能是由于行与 Grok 模式不匹配。",
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts
index 24437e02e6907..3a859249fe234 100644
--- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts
+++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts
@@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
results: {
title: 'artificial_server_log',
- numberOfFields: 4,
+ highlightedText: true,
},
metricFields: [
{
@@ -104,7 +104,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
results: {
title: 'geo_file.csv',
- numberOfFields: 3,
+ highlightedText: false,
},
metricFields: [],
nonMetricFields: [
@@ -146,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
results: {
title: 'missing_end_of_file_newline.csv',
- numberOfFields: 3,
+ highlightedText: false,
},
metricFields: [
{
@@ -217,6 +217,13 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('displays the components of the file details page');
await ml.dataVisualizerFileBased.assertFileTitle(testData.expected.results.title);
await ml.dataVisualizerFileBased.assertFileContentPanelExists();
+ await ml.dataVisualizerFileBased.assertFileContentHighlightingSwitchExists(
+ testData.expected.results.highlightedText
+ );
+ await ml.dataVisualizerFileBased.assertFileContentHighlighting(
+ testData.expected.results.highlightedText,
+ testData.expected.totalFieldsCount - 1 // -1 for the message field
+ );
await ml.dataVisualizerFileBased.assertSummaryPanelExists();
await ml.dataVisualizerFileBased.assertFileStatsPanelExists();
diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts
index 0b8effd17cbb0..df95eddd957f8 100644
--- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts
+++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts
@@ -49,6 +49,32 @@ export function MachineLearningDataVisualizerFileBasedProvider(
await testSubjects.existOrFail('dataVisualizerFileFileContentPanel');
},
+ async assertFileContentHighlightingSwitchExists(exist: boolean) {
+ const tabs = await testSubjects.findAll('dataVisualizerFileContentsHighlightingSwitch');
+ expect(tabs.length).to.eql(
+ exist ? 1 : 0,
+ `Expected file content highlighting switch to ${exist ? 'exist' : 'not exist'}, but found ${
+ tabs.length
+ }`
+ );
+ },
+
+ async assertFileContentHighlighting(highlighted: boolean, numberOfFields: number) {
+ const lines = await testSubjects.findAll('dataVisualizerHighlightedLine', 1000);
+ const linesExist = lines.length > 0;
+ expect(linesExist).to.eql(
+ highlighted,
+ `Expected file content highlighting to be '${highlighted ? 'enabled' : 'disabled'}'`
+ );
+ const expectedNumberOfFields = highlighted ? numberOfFields : 0;
+ const foundFields = (await lines[0]?.findAllByTestSubject('dataVisualizerFieldBadge')) ?? [];
+
+ expect(foundFields.length).to.eql(
+ expectedNumberOfFields,
+ `Expected ${expectedNumberOfFields} fields to be highlighted, but found ${foundFields.length}`
+ );
+ },
+
async assertSummaryPanelExists() {
await testSubjects.existOrFail('dataVisualizerFileSummaryPanel');
},
From d492fb7300cfe86ee79e0ccee8743f4b57dcc351 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 8 Feb 2024 11:44:25 +0200
Subject: [PATCH 005/104] [ES|QL] Clicking the editor closes the documentation
popover (#176394)
## Summary
Part of https://github.com/elastic/kibana/issues/166907
Fixes the problem with the eui popovers and the monaco editor. I am not
sure why it is happening. I think it is due to the third party library
that eui uses to detects the outside clicks.
What I did to fix it:
- Wrap the documentation popover in an EuiOutsideClickDetector
- I force monaco editor to focus onMoyseDown
![meow](https://github.com/elastic/kibana/assets/17003240/f432608d-0801-4a68-b327-da1e8dec9f9a)
This solves the problem on the documentation popover but not on the
SuperDatePicker because I don't have a way to force it to close. I will
ping the EUI team
---
.../src/components/documentation_popover.tsx | 70 +++++++++++--------
.../src/text_based_languages_editor.tsx | 11 +++
2 files changed, 52 insertions(+), 29 deletions(-)
diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx b/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx
index db66d69d7173f..735c567dbb4d5 100644
--- a/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx
+++ b/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx
@@ -7,7 +7,13 @@
*/
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiPopover, EuiToolTip, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui';
+import {
+ EuiPopover,
+ EuiToolTip,
+ EuiButtonIcon,
+ EuiButtonIconProps,
+ EuiOutsideClickDetector,
+} from '@elastic/eui';
import {
type LanguageDocumentationSections,
LanguageDocumentationPopoverContent,
@@ -33,35 +39,41 @@ function DocumentationPopover({
}, [isHelpOpen]);
return (
- setIsHelpOpen(false)}
- button={
-
-
-
- }
+ {
+ setIsHelpOpen(false);
+ }}
>
-
-
+ setIsHelpOpen(false)}
+ button={
+
+
+
+ }
+ >
+
+
+
);
}
diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
index 146e020d4fe82..15adf2f293bb8 100644
--- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
+++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
@@ -815,6 +815,17 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
}
});
+ // this is fixing a bug between the EUIPopover and the monaco editor
+ // when the user clicks the editor, we force it to focus and the onDidFocusEditorText
+ // to fire, the timeout is needed because otherwise it refocuses on the popover icon
+ // and the user needs to click again the editor.
+ // IMPORTANT: The popover needs to be wrapped with the EuiOutsideClickDetector component.
+ editor.onMouseDown(() => {
+ setTimeout(() => {
+ editor.focus();
+ }, 100);
+ });
+
editor.onDidFocusEditorText(() => {
onEditorFocus();
});
From 6bb2bd3d5cc45bf1cb5e486c5e0f68c9aa0fe4c5 Mon Sep 17 00:00:00 2001
From: Navarone Feekery <13634519+navarone-feekery@users.noreply.github.com>
Date: Thu, 8 Feb 2024 10:57:19 +0100
Subject: [PATCH 006/104] [Search] Remove API keys when deleting connectors
(#176407)
When deleting connectors or indices in Search:
- Invalidate the API key if a connector has an `api_key_id` value
- Delete the connector secret if a connector has an `api_key_secret_id`
value
---
.../lib/delete_connector_secret.test.ts | 39 +++++++++++++++++++
.../lib/delete_connector_secret.ts | 17 ++++++++
packages/kbn-search-connectors/lib/index.ts | 1 +
.../routes/enterprise_search/connectors.ts | 18 ++++++---
.../routes/enterprise_search/indices.ts | 8 +++-
5 files changed, 77 insertions(+), 6 deletions(-)
create mode 100644 packages/kbn-search-connectors/lib/delete_connector_secret.test.ts
create mode 100644 packages/kbn-search-connectors/lib/delete_connector_secret.ts
diff --git a/packages/kbn-search-connectors/lib/delete_connector_secret.test.ts b/packages/kbn-search-connectors/lib/delete_connector_secret.test.ts
new file mode 100644
index 0000000000000..164a37f3ccb57
--- /dev/null
+++ b/packages/kbn-search-connectors/lib/delete_connector_secret.test.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+
+import { deleteConnectorSecret } from './delete_connector_secret';
+
+describe('deleteConnectorSecret lib function', () => {
+ const mockClient = {
+ transport: {
+ request: jest.fn(),
+ },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ });
+
+ it('should delete a connector secret', async () => {
+ mockClient.transport.request.mockImplementation(() => ({
+ result: 'deleted',
+ }));
+
+ await expect(
+ deleteConnectorSecret(mockClient as unknown as ElasticsearchClient, 'secret-id')
+ ).resolves.toEqual({ result: 'deleted' });
+ expect(mockClient.transport.request).toHaveBeenCalledWith({
+ method: 'DELETE',
+ path: '/_connector/_secret/secret-id',
+ });
+ jest.useRealTimers();
+ });
+});
diff --git a/packages/kbn-search-connectors/lib/delete_connector_secret.ts b/packages/kbn-search-connectors/lib/delete_connector_secret.ts
new file mode 100644
index 0000000000000..d3ecbe8da73f5
--- /dev/null
+++ b/packages/kbn-search-connectors/lib/delete_connector_secret.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import { ConnectorsAPIUpdateResponse } from '../types/connectors_api';
+
+export const deleteConnectorSecret = async (client: ElasticsearchClient, id: string) => {
+ return await client.transport.request({
+ method: 'DELETE',
+ path: `/_connector/_secret/${id}`,
+ });
+};
diff --git a/packages/kbn-search-connectors/lib/index.ts b/packages/kbn-search-connectors/lib/index.ts
index 3e929d5bc6834..e0a1caea66422 100644
--- a/packages/kbn-search-connectors/lib/index.ts
+++ b/packages/kbn-search-connectors/lib/index.ts
@@ -11,6 +11,7 @@ export * from './create_connector';
export * from './create_connector_document';
export * from './create_connector_secret';
export * from './delete_connector';
+export * from './delete_connector_secret';
export * from './fetch_connectors';
export * from './fetch_sync_jobs';
export * from './update_filtering';
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
index 39657c97c6202..4226e4326ce0a 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
@@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import {
deleteConnectorById,
+ deleteConnectorSecret,
fetchConnectorById,
fetchConnectors,
fetchSyncJobs,
@@ -589,18 +590,25 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
const { shouldDeleteIndex } = request.query;
let connectorResponse;
- let indexNameToDelete;
try {
- if (shouldDeleteIndex) {
- const connector = await fetchConnectorById(client.asCurrentUser, connectorId);
- indexNameToDelete = connector?.value.index_name;
- }
+ const connector = await fetchConnectorById(client.asCurrentUser, connectorId);
+ const indexNameToDelete = shouldDeleteIndex ? connector?.value.index_name : null;
+ const apiKeyId = connector?.value.api_key_id;
+ const secretId = connector?.value.api_key_secret_id;
+
connectorResponse = await deleteConnectorById(client.asCurrentUser, connectorId);
+
if (indexNameToDelete) {
await deleteIndexPipelines(client, indexNameToDelete);
await deleteAccessControlIndex(client, indexNameToDelete);
await client.asCurrentUser.indices.delete({ index: indexNameToDelete });
}
+ if (apiKeyId) {
+ await client.asCurrentUser.security.invalidateApiKey({ ids: [apiKeyId] });
+ }
+ if (secretId) {
+ await deleteConnectorSecret(client.asCurrentUser, secretId);
+ }
} catch (error) {
if (isResourceNotFoundException(error)) {
return createError({
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
index d96130fb02472..fdbeca1e82ff9 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
@@ -14,7 +14,7 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
-import { deleteConnectorById } from '@kbn/search-connectors';
+import { deleteConnectorById, deleteConnectorSecret } from '@kbn/search-connectors';
import {
fetchConnectorByIndexName,
fetchConnectors,
@@ -204,6 +204,12 @@ export function registerIndexRoutes({
if (connector) {
await deleteConnectorById(client.asCurrentUser, connector.id);
+ if (connector.api_key_id) {
+ await client.asCurrentUser.security.invalidateApiKey({ ids: [connector.api_key_id] });
+ }
+ if (connector.api_key_secret_id) {
+ await deleteConnectorSecret(client.asCurrentUser, connector.api_key_secret_id);
+ }
}
await deleteIndexPipelines(client, indexName);
From b1b36bdd23324bd12b74267287cbe368c1504eaa Mon Sep 17 00:00:00 2001
From: Maryam Saeidi
Date: Thu, 8 Feb 2024 10:58:43 +0100
Subject: [PATCH 007/104] [Custom threshold] Add threshold to the custom
threshold alert document (#176043)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Part of #175136
## Summary
This PR persists the threshold value in the AAD document. Now, when
creating a custom threshold rule, you should be able to see the value in
the alert table:
![image](https://github.com/elastic/kibana/assets/12370520/a5b3ddaf-f218-48c0-b73c-d4c7cc72506e)
## 🧪 How to test
- Create a custom threshold rule with one or multiple conditions
- When the related alert fires, you should be able to see the threshold
value (unformatted) in the alert table
---
.../alerts_table/common/render_cell_value.tsx | 5 +-
.../custom_threshold_executor.test.ts | 3 ++
.../custom_threshold_executor.ts | 20 +++----
.../custom_threshold/lib/get_values.test.ts | 28 ++++++++++
.../rules/custom_threshold/lib/get_values.ts | 29 ++++++++++
.../mocks/custom_threshold_alert_result.ts | 54 +++++++++++++++++++
.../mocks/custom_threshold_metric_params.ts | 51 ++++++++++++++++++
.../lib/rules/custom_threshold/types.ts | 16 +++++-
.../custom_threshold_rule/avg_pct_fired.ts | 2 +-
.../custom_threshold_rule/avg_us_fired.ts | 4 +-
.../custom_eq_avg_bytes_fired.ts | 2 +-
.../documents_count_fired.ts | 2 +-
.../custom_threshold_rule/group_by_fired.ts | 2 +-
.../custom_threshold_rule/avg_pct_fired.ts | 2 +-
.../custom_eq_avg_bytes_fired.ts | 2 +-
.../documents_count_fired.ts | 2 +-
.../custom_threshold_rule/group_by_fired.ts | 2 +-
17 files changed, 203 insertions(+), 23 deletions(-)
create mode 100644 x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.test.ts
create mode 100644 x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.ts
create mode 100644 x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_alert_result.ts
create mode 100644 x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_metric_params.ts
diff --git a/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx b/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx
index 3bde70900d334..d68b11115396e 100644
--- a/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx
+++ b/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx
@@ -100,11 +100,12 @@ export const getRenderCellValue = ({
case ALERT_SEVERITY:
return ;
case ALERT_EVALUATION_VALUE:
- const values = getMappedNonEcsValue({
+ const valuesField = getMappedNonEcsValue({
data,
fieldName: ALERT_EVALUATION_VALUES,
});
- return values ? values : value;
+ const values = getRenderValue(valuesField);
+ return valuesField ? values : value;
case ALERT_REASON:
const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {});
const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs);
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
index 9e3eab1e8a054..51fc7e7227cdf 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
@@ -75,11 +75,13 @@ const mockOptions = {
previousStartedAt: null,
params: {
searchConfiguration: {
+ index: {},
query: {
query: mockQuery,
language: 'kuery',
},
},
+ alertOnNoData: true,
},
state: {
wrapped: initialRuleState,
@@ -573,6 +575,7 @@ describe('The custom threshold alert type', () => {
},
],
searchConfiguration: {
+ index: {},
query: {
query: filterQuery,
language: 'kuery',
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
index 39dbade62ce62..b59c3f532daea 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
@@ -10,6 +10,7 @@ import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability';
import {
ALERT_ACTION_GROUP,
ALERT_EVALUATION_VALUES,
+ ALERT_EVALUATION_THRESHOLD,
ALERT_REASON,
ALERT_GROUP,
} from '@kbn/rule-data-utils';
@@ -17,13 +18,14 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
import { RecoveredActionGroup } from '@kbn/alerting-plugin/common';
import { IBasePath, Logger } from '@kbn/core/server';
import { LifecycleRuleExecutor } from '@kbn/rule-registry-plugin/server';
+import { getEvaluationValues, getThreshold } from './lib/get_values';
import { AlertsLocatorParams, getAlertUrl } from '../../../../common';
import { getViewInAppUrl } from '../../../../common/custom_threshold_rule/get_view_in_app_url';
import { ObservabilityConfig } from '../../..';
import { FIRED_ACTIONS_ID, NO_DATA_ACTIONS_ID, UNGROUPED_FACTORY_KEY } from './constants';
import {
AlertStates,
- CustomThresholdRuleParams,
+ CustomThresholdRuleTypeParams,
CustomThresholdRuleTypeState,
CustomThresholdAlertState,
CustomThresholdAlertContext,
@@ -66,7 +68,7 @@ export const createCustomThresholdExecutor = ({
config: ObservabilityConfig;
locators: CustomThresholdLocators;
}): LifecycleRuleExecutor<
- CustomThresholdRuleParams,
+ CustomThresholdRuleTypeParams,
CustomThresholdRuleTypeState,
CustomThresholdAlertState,
CustomThresholdAlertContext,
@@ -108,6 +110,7 @@ export const createCustomThresholdExecutor = ({
actionGroup,
additionalContext,
evaluationValues,
+ threshold,
group
) =>
alertWithLifecycle({
@@ -116,6 +119,7 @@ export const createCustomThresholdExecutor = ({
[ALERT_REASON]: reason,
[ALERT_ACTION_GROUP]: actionGroup,
[ALERT_EVALUATION_VALUES]: evaluationValues,
+ [ALERT_EVALUATION_THRESHOLD]: threshold,
[ALERT_GROUP]: group,
...flattenAdditionalContext(additionalContext),
},
@@ -139,7 +143,7 @@ export const createCustomThresholdExecutor = ({
? state.missingGroups
: [];
- const initialSearchSource = await searchSourceClient.create(params.searchConfiguration!);
+ const initialSearchSource = await searchSourceClient.create(params.searchConfiguration);
const dataView = initialSearchSource.getField('index')!;
const { id: dataViewId, timeFieldName } = dataView;
const dataViewIndexPattern = dataView.getIndexPattern();
@@ -241,6 +245,8 @@ export const createCustomThresholdExecutor = ({
if (reason) {
const timestamp = startedAt.toISOString();
+ const threshold = getThreshold(criteria);
+ const evaluationValues = getEvaluationValues(alertResults, group);
const actionGroupId: CustomThresholdActionGroup =
nextState === AlertStates.OK
? RecoveredActionGroup.id
@@ -258,19 +264,13 @@ export const createCustomThresholdExecutor = ({
new Set([...(additionalContext.tags ?? []), ...options.rule.tags])
);
- const evaluationValues = alertResults.reduce((acc: Array, result) => {
- if (result[group]) {
- acc.push(result[group].currentValue);
- }
- return acc;
- }, []);
-
const alert = alertFactory(
`${group}`,
reason,
actionGroupId,
additionalContext,
evaluationValues,
+ threshold,
groupByKeysObjectMapping[group]
);
const alertUuid = getAlertUuid(group);
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.test.ts
new file mode 100644
index 0000000000000..ddf2b7b1b1a45
--- /dev/null
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.test.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { alertResultsMultipleConditions } from '../mocks/custom_threshold_alert_result';
+import { criteriaMultipleConditions } from '../mocks/custom_threshold_metric_params';
+import { getEvaluationValues, getThreshold } from './get_values';
+
+describe('getValue helpers', () => {
+ describe('getThreshold', () => {
+ test('should return threshold for one condition', () => {
+ expect(getThreshold([criteriaMultipleConditions[1]])).toEqual([4, 5]);
+ });
+
+ test('should return threshold for multiple conditions', () => {
+ expect(getThreshold(criteriaMultipleConditions)).toEqual([1, 2, 4, 5]);
+ });
+ });
+
+ describe('getEvaluationValues', () => {
+ test('should return evaluation values ', () => {
+ expect(getEvaluationValues(alertResultsMultipleConditions, '*')).toEqual([1.0, 3.0]);
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.ts
new file mode 100644
index 0000000000000..13c0cabb9b590
--- /dev/null
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types';
+import { Evaluation } from './evaluate_rule';
+
+export const getEvaluationValues = (
+ alertResults: Array>,
+ group: string
+): Array => {
+ return alertResults.reduce((acc: Array, result) => {
+ if (result[group]) {
+ acc.push(result[group].currentValue);
+ }
+ return acc;
+ }, []);
+};
+
+export const getThreshold = (criteria: CustomMetricExpressionParams[]): number[] => {
+ const threshold = criteria.map((c) => c.threshold);
+
+ return threshold.reduce((acc: number[], t) => {
+ return acc.concat(...t);
+ }, []);
+};
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_alert_result.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_alert_result.ts
new file mode 100644
index 0000000000000..b8ab169b7a90d
--- /dev/null
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_alert_result.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 {
+ Aggregators,
+ Comparator,
+ CustomMetricExpressionParams,
+} from '../../../../../common/custom_threshold_rule/types';
+import { Evaluation } from '../lib/evaluate_rule';
+
+const customThresholdNonCountCriterion: CustomMetricExpressionParams = {
+ comparator: Comparator.GT,
+ metrics: [
+ {
+ aggType: Aggregators.AVERAGE,
+ name: 'A',
+ field: 'test.metric.1',
+ },
+ ],
+ timeSize: 1,
+ timeUnit: 'm',
+ threshold: [0],
+};
+
+export const alertResultsMultipleConditions: Array> = [
+ {
+ '*': {
+ ...customThresholdNonCountCriterion,
+ comparator: Comparator.GT,
+ threshold: [0.75],
+ currentValue: 1.0,
+ timestamp: new Date().toISOString(),
+ shouldFire: true,
+ isNoData: false,
+ bucketKey: { groupBy0: '*' },
+ },
+ },
+ {
+ '*': {
+ ...customThresholdNonCountCriterion,
+ comparator: Comparator.GT,
+ threshold: [0.75],
+ currentValue: 3.0,
+ timestamp: new Date().toISOString(),
+ shouldFire: true,
+ isNoData: false,
+ bucketKey: { groupBy0: '*' },
+ },
+ },
+];
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_metric_params.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_metric_params.ts
new file mode 100644
index 0000000000000..9d09c90859659
--- /dev/null
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_metric_params.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 {
+ Aggregators,
+ Comparator,
+ CustomMetricExpressionParams,
+} from '../../../../../common/custom_threshold_rule/types';
+
+export const criteriaMultipleConditions: CustomMetricExpressionParams[] = [
+ {
+ metrics: [
+ {
+ name: 'A',
+ aggType: Aggregators.AVERAGE,
+ field: 'system.is.a.good.puppy.dog',
+ },
+ {
+ name: 'B',
+ aggType: Aggregators.AVERAGE,
+ field: 'system.is.a.bad.kitty',
+ },
+ ],
+ timeUnit: 'm',
+ timeSize: 1,
+ threshold: [1, 2],
+ comparator: Comparator.GT,
+ },
+ {
+ metrics: [
+ {
+ name: 'A',
+ aggType: Aggregators.AVERAGE,
+ field: 'system.is.a.good.puppy.dog',
+ },
+ {
+ name: 'B',
+ aggType: Aggregators.AVERAGE,
+ field: 'system.is.a.bad.kitty',
+ },
+ ],
+ timeUnit: 'm',
+ timeSize: 1,
+ threshold: [4, 5],
+ comparator: Comparator.GT,
+ },
+];
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts
index 97881f55d8d32..f688088d6b816 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts
@@ -14,6 +14,8 @@ import {
} from '@kbn/alerting-plugin/common';
import { Alert } from '@kbn/alerting-plugin/server';
import { TypeOf } from '@kbn/config-schema';
+import { DataViewSpec } from '@kbn/data-views-plugin/common';
+import { CustomMetricExpressionParams } from '../../../../common/custom_threshold_rule/types';
import { FIRED_ACTIONS_ID, NO_DATA_ACTIONS_ID, FIRED_ACTION, NO_DATA_ACTION } from './constants';
import { MissingGroupsRecord } from './lib/check_missing_group';
import { AdditionalContext } from './utils';
@@ -28,13 +30,22 @@ export enum AlertStates {
// Executor types
export type SearchConfigurationType = TypeOf;
+export type RuleTypeParams = Record;
+
+export interface CustomThresholdRuleTypeParams extends RuleTypeParams {
+ criteria: CustomMetricExpressionParams[];
+ // Index will be a data view spec after extracting references
+ searchConfiguration: Omit & { index: DataViewSpec };
+ groupBy?: string | string[];
+ alertOnNoData: boolean;
+ alertOnGroupDisappear?: boolean;
+}
-export type CustomThresholdRuleParams = Record;
export type CustomThresholdRuleTypeState = RuleTypeState & {
lastRunTimestamp?: number;
missingGroups?: Array;
groupBy?: string | string[];
- searchConfiguration?: SearchConfigurationType;
+ searchConfiguration?: Omit & { index: DataViewSpec };
};
export type CustomThresholdAlertState = AlertState; // no specific instance state used
export type CustomThresholdAlertContext = AlertContext & {
@@ -64,6 +75,7 @@ export type CustomThresholdAlertFactory = (
actionGroup: CustomThresholdActionGroup,
additionalContext?: AdditionalContext | null,
evaluationValues?: Array,
+ threshold?: Array,
group?: Group
) => CustomThresholdAlert;
diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts
index 9c41ca3d150b3..929913f15f39f 100644
--- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts
+++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_pct_fired.ts
@@ -209,7 +209,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open');
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.5]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts
index 7d9c7bf7e657e..1a32a0af3612e 100644
--- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts
+++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/avg_us_fired.ts
@@ -187,7 +187,9 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open');
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
-
+ expect(resp.hits.hits[0]._source)
+ .property('kibana.alert.evaluation.threshold')
+ .eql([7500000]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts
index 06eaea7f21c9c..d1c8a2927837c 100644
--- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts
+++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts
@@ -206,7 +206,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open');
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.9]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts
index 0185d7b8ea05e..43029573defd8 100644
--- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts
+++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts
@@ -208,7 +208,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('event.action', 'open');
expect(resp.hits.hits[0]._source).not.have.property('kibana.alert.group');
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([1, 2]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts
index ca73e43114441..55f2d61be2b0e 100644
--- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts
+++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts
@@ -230,7 +230,7 @@ export default function ({ getService }: FtrProviderContext) {
value: 'container-0',
},
]);
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.2]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts
index 32dd3e263998b..47ed06c1ad440 100644
--- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts
@@ -191,7 +191,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open');
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.5]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts
index 95d9608d0a841..8b6d76a380748 100644
--- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts
@@ -200,7 +200,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open');
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.9]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts
index 519c0329e6390..15c34602e0a78 100644
--- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts
@@ -191,7 +191,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open');
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([1, 2]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts
index ccd2aa6edaeaa..264057cacff1c 100644
--- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts
@@ -217,7 +217,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('container.id', 'container-0');
expect(resp.hits.hits[0]._source).property('container.name', 'container-name');
expect(resp.hits.hits[0]._source).not.property('container.cpu');
-
+ expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.2]);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
From 52f35fca662bffe99ee3ba431f7d1e94a50ac903 Mon Sep 17 00:00:00 2001
From: Coen Warmer
Date: Thu, 8 Feb 2024 11:30:01 +0100
Subject: [PATCH 008/104] [Observability AI Assistant] Chat tweaks + keyboard
shortcut (#176350)
---
.../action_menu_item/action_menu_item.tsx | 16 +-
.../components/chat/chat_actions_menu.tsx | 28 ++--
.../public/components/chat/chat_body.tsx | 23 ++-
.../public/components/chat/chat_flyout.tsx | 138 +++++++++++++-----
.../public/components/chat/chat_header.tsx | 128 +++++++++++++---
.../use_observability_ai_assistant_router.ts | 37 ++++-
6 files changed, 293 insertions(+), 77 deletions(-)
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx
index f11f2c8b56bc6..c357d9d22e8f6 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import React, { useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider';
@@ -30,6 +30,20 @@ export function ObservabilityAIAssistantActionMenuItem() {
const initialMessages = useMemo(() => [], []);
+ useEffect(() => {
+ const keyboardListener = (event: KeyboardEvent) => {
+ if (event.ctrlKey && event.code === 'Semicolon') {
+ setIsOpen(true);
+ }
+ };
+
+ window.addEventListener('keypress', keyboardListener);
+
+ return () => {
+ window.removeEventListener('keypress', keyboardListener);
+ };
+ }, []);
+
if (!service.isEnabled()) {
return null;
}
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx
index 4186f2c70c04d..f9635f5808072 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx
@@ -7,7 +7,7 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui';
+import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover, EuiToolTip } from '@elastic/eui';
import { useKibana } from '../../hooks/use_kibana';
import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router';
import { getSettingsHref } from '../../utils/get_settings_href';
@@ -52,16 +52,24 @@ export function ChatActionsMenu({
+ display="block"
+ >
+
+
}
panelPaddingSize="none"
closePopover={toggleActionsMenu}
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx
index 373e35641ff8f..8ed26d71acc58 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx
@@ -43,6 +43,7 @@ import {
import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n';
import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation';
import { TELEMETRY, sendEvent } from '../../analytics';
+import { FlyoutWidthMode } from './chat_flyout';
const fullHeightClassName = css`
height: 100%;
@@ -93,27 +94,31 @@ const animClassName = css`
const PADDING_AND_BORDER = 32;
export function ChatBody({
- initialTitle,
- initialMessages,
- initialConversationId,
+ chatFlyoutSecondSlotHandler,
connectors,
- knowledgeBase,
currentUser,
+ flyoutWidthMode,
+ initialConversationId,
+ initialMessages,
+ initialTitle,
+ knowledgeBase,
showLinkToConversationsApp,
startedFrom,
- chatFlyoutSecondSlotHandler,
onConversationUpdate,
+ onToggleFlyoutWidthMode,
}: {
+ chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler;
+ connectors: UseGenAIConnectorsResult;
+ currentUser?: Pick;
+ flyoutWidthMode?: FlyoutWidthMode;
initialTitle?: string;
initialMessages?: Message[];
initialConversationId?: string;
- connectors: UseGenAIConnectorsResult;
knowledgeBase: UseKnowledgeBaseResult;
- currentUser?: Pick;
showLinkToConversationsApp: boolean;
startedFrom?: StartedFrom;
- chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler;
onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void;
+ onToggleFlyoutWidthMode?: (flyoutWidthMode: FlyoutWidthMode) => void;
}) {
const license = useLicense();
const hasCorrectLicense = license?.hasAtLeast('enterprise');
@@ -455,6 +460,7 @@ export function ChatBody({
? conversation.value.conversation.id
: undefined
}
+ flyoutWidthMode={flyoutWidthMode}
licenseInvalid={!hasCorrectLicense && !initialConversationId}
loading={isLoading}
showLinkToConversationsApp={showLinkToConversationsApp}
@@ -463,6 +469,7 @@ export function ChatBody({
onSaveTitle={(newTitle) => {
saveTitle(newTitle);
}}
+ onToggleFlyoutWidthMode={onToggleFlyoutWidthMode}
/>
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx
index 83c1496befa4f..6823153397ca4 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx
@@ -9,7 +9,15 @@ import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { v4 } from 'uuid';
import { css } from '@emotion/css';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFlyout, useEuiTheme } from '@elastic/eui';
+import {
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiPopover,
+ EuiToolTip,
+ useEuiTheme,
+} from '@elastic/eui';
import { useForceUpdate } from '../../hooks/use_force_update';
import { useCurrentUser } from '../../hooks/use_current_user';
import { useGenAIConnectors } from '../../hooks/use_genai_connectors';
@@ -25,6 +33,8 @@ const CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED = 34;
const SIDEBAR_WIDTH = 400;
+export type FlyoutWidthMode = 'side' | 'full';
+
export function ChatFlyout({
initialTitle,
initialMessages,
@@ -48,26 +58,36 @@ export function ChatFlyout({
const [conversationId, setConversationId] = useState(undefined);
- const [expanded, setExpanded] = useState(false);
+ const [flyoutWidthMode, setFlyoutWidthMode] = useState('side');
+
+ const [conversationsExpanded, setConversationsExpanded] = useState(false);
+
const [secondSlotContainer, setSecondSlotContainer] = useState(null);
const [isSecondSlotVisible, setIsSecondSlotVisible] = useState(false);
const sidebarClass = css`
- max-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px;
- min-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px;
+ max-width: ${conversationsExpanded
+ ? CONVERSATIONS_SIDEBAR_WIDTH
+ : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px;
+ min-width: ${conversationsExpanded
+ ? CONVERSATIONS_SIDEBAR_WIDTH
+ : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px;
border-right: solid 1px ${euiTheme.border.color};
`;
- const expandButtonClassName = css`
+ const expandButtonContainerClassName = css`
position: absolute;
margin-top: 16px;
- margin-left: ${expanded
+ margin-left: ${conversationsExpanded
? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED
: 5}px;
- padding: ${euiTheme.size.s};
z-index: 1;
`;
+ const expandButtonClassName = css`
+ color: ${euiTheme.colors.primary};
+ `;
+
const containerClassName = css`
height: 100%;
`;
@@ -79,10 +99,9 @@ export function ChatFlyout({
const newChatButtonClassName = css`
position: absolute;
bottom: 31px;
- margin-left: ${expanded
+ margin-left: ${conversationsExpanded
? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED
: 5}px;
- padding: ${euiTheme.size.s};
z-index: 1;
`;
@@ -110,12 +129,20 @@ export function ChatFlyout({
}
};
+ const handleToggleFlyoutWidthMode = (newFlyoutWidthMode: FlyoutWidthMode) => {
+ setFlyoutWidthMode(newFlyoutWidthMode);
+ };
+
return isOpen ? (
{
onClose();
@@ -127,19 +154,40 @@ export function ChatFlyout({
>
- setExpanded(!expanded)}
+
+ setConversationsExpanded(!conversationsExpanded)}
+ />
+
+ }
/>
- {expanded ? (
+ {conversationsExpanded ? (
) : (
-
+
+
+ }
className={newChatButtonClassName}
- data-test-subj="observabilityAiAssistantNewChatFlyoutButton"
- iconType="plusInCircle"
- onClick={handleClickNewChat}
/>
)}
@@ -163,21 +224,23 @@ export function ChatFlyout({
{
setConversationId(conversation.conversation.id);
}}
- chatFlyoutSecondSlotHandler={{
- container: secondSlotContainer,
- setVisibility: setIsSecondSlotVisible,
- }}
- showLinkToConversationsApp
+ onToggleFlyoutWidthMode={handleToggleFlyoutWidthMode}
/>
@@ -204,10 +267,15 @@ export function ChatFlyout({
const getFlyoutWidth = ({
expanded,
isSecondSlotVisible,
+ flyoutWidthMode,
}: {
expanded: boolean;
isSecondSlotVisible: boolean;
+ flyoutWidthMode?: FlyoutWidthMode;
}) => {
+ if (flyoutWidthMode === 'full') {
+ return '100%';
+ }
if (!expanded && !isSecondSlotVisible) {
return '40vw';
}
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx
index cd4dc0d824590..de8a80928207b 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx
@@ -6,11 +6,14 @@
*/
import React, { useEffect, useState } from 'react';
import {
+ EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiInlineEditTitle,
EuiLoadingSpinner,
EuiPanel,
+ EuiPopover,
+ EuiToolTip,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -18,6 +21,8 @@ import { css } from '@emotion/css';
import { AssistantAvatar } from '../assistant_avatar';
import { ChatActionsMenu } from './chat_actions_menu';
import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
+import type { FlyoutWidthMode } from './chat_flyout';
+import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router';
// needed to prevent InlineTextEdit component from expanding container
const minWidthClassName = css`
@@ -30,36 +35,54 @@ const chatHeaderClassName = css`
`;
export function ChatHeader({
- title,
- loading,
- licenseInvalid,
connectors,
conversationId,
+ flyoutWidthMode,
+ licenseInvalid,
+ loading,
showLinkToConversationsApp,
+ title,
onCopyConversation,
onSaveTitle,
+ onToggleFlyoutWidthMode,
}: {
- title: string;
- loading: boolean;
- licenseInvalid: boolean;
connectors: UseGenAIConnectorsResult;
conversationId?: string;
+ flyoutWidthMode?: FlyoutWidthMode;
+ licenseInvalid: boolean;
+ loading: boolean;
showLinkToConversationsApp: boolean;
+ title: string;
onCopyConversation: () => void;
onSaveTitle: (title: string) => void;
+ onToggleFlyoutWidthMode?: (newFlyoutWidthMode: FlyoutWidthMode) => void;
}) {
const theme = useEuiTheme();
+ const router = useObservabilityAIAssistantRouter();
+
const [newTitle, setNewTitle] = useState(title);
useEffect(() => {
setNewTitle(title);
}, [title]);
- const chatActionsMenuWrapper = css`
- position: absolute;
- right: 46px;
- `;
+ const handleToggleFlyoutWidthMode = () => {
+ onToggleFlyoutWidthMode?.(flyoutWidthMode === 'side' ? 'full' : 'side');
+ };
+
+ const handleNavigateToConversations = () => {
+ if (conversationId) {
+ router.navigateToConversationsApp('/conversations/{conversationId}', {
+ path: {
+ conversationId,
+ },
+ query: {},
+ });
+ } else {
+ router.navigateToConversationsApp('/conversations/new', { path: {}, query: {} });
+ }
+ };
return (
-
-
+
+
+
+ {flyoutWidthMode && onToggleFlyoutWidthMode ? (
+ <>
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+ }
+ />
+
+ >
+ ) : null}
+
+
+
+
+
diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts
index 160a4835d0ffb..afdae21c91a8d 100644
--- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts
+++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts
@@ -21,13 +21,20 @@ interface StatefulObservabilityAIAssistantRouter extends ObservabilityAIAssistan
path: T,
...params: TypeAsArgs>
): void;
+ navigateToConversationsApp>(
+ path: T,
+ ...params: TypeAsArgs>
+ ): void;
}
export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssistantRouter {
const history = useHistory();
const {
- services: { http },
+ services: {
+ http,
+ application: { navigateToApp },
+ },
} = useKibana();
const link = (...args: any[]) => {
@@ -43,6 +50,32 @@ export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssi
history.push(next);
},
+ navigateToConversationsApp: (path, ...args) => {
+ const [_, route, routeParam] = path.split('/');
+
+ const sanitized = routeParam.replace('{', '').replace('}', '');
+
+ const pathKey = args[0]?.path;
+
+ if (typeof pathKey !== 'object') {
+ return;
+ }
+
+ if (Object.keys(pathKey).length === 0) {
+ navigateToApp('observabilityAIAssistant', {
+ path: route,
+ });
+ return;
+ }
+
+ if (Object.keys(pathKey).length === 1) {
+ navigateToApp('observabilityAIAssistant', {
+ // @ts-expect-error
+ path: `${route}/${pathKey[sanitized]}`,
+ });
+ return;
+ }
+ },
replace: (path, ...args) => {
const next = link(path, ...args);
history.replace(next);
@@ -51,6 +84,6 @@ export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssi
return http.basePath.prepend('/app/observabilityAIAssistant' + link(path, ...args));
},
}),
- [http, history]
+ [history, navigateToApp, http.basePath]
);
}
From 6076d1b39588867f33cd258375120f0b813447ce Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 8 Feb 2024 12:56:55 +0200
Subject: [PATCH 009/104] [ES|QL] Adds link on the documentation popover to
navigate users to our external docs (#176377)
## Summary
Part of https://github.com/elastic/kibana/issues/173495
Adds a link to the external documents in the popover used for ES|QL
reference. The request is here
https://github.com/elastic/kibana/issues/173495#issuecomment-1916896737
---
.../src/components/documentation_content.tsx | 37 ++++++++++++++++---
.../src/components/documentation_popover.tsx | 3 ++
.../src/text_based_languages_editor.tsx | 9 ++++-
3 files changed, 42 insertions(+), 7 deletions(-)
diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx b/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx
index 0f24e233a4f28..daf52aba46727 100644
--- a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx
+++ b/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx
@@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
- EuiLink,
EuiPopoverTitle,
EuiText,
EuiListGroupItem,
@@ -19,6 +18,7 @@ import {
EuiFieldSearch,
EuiHighlight,
EuiSpacer,
+ EuiLink,
} from '@elastic/eui';
import { elementToString } from '../utils/element_to_string';
@@ -38,9 +38,16 @@ interface DocumentationProps {
sections?: LanguageDocumentationSections;
// if sets to true, allows searching in the markdown description
searchInDescription?: boolean;
+ // if set, a link will appear on the top right corner
+ linkToDocumentation?: string;
}
-function DocumentationContent({ language, sections, searchInDescription }: DocumentationProps) {
+function DocumentationContent({
+ language,
+ sections,
+ searchInDescription,
+ linkToDocumentation,
+}: DocumentationProps) {
const [selectedSection, setSelectedSection] = useState();
const scrollTargets = useRef>({});
@@ -83,10 +90,28 @@ function DocumentationContent({ language, sections, searchInDescription }: Docum
paddingSize="m"
data-test-subj="language-documentation-title"
>
- {i18n.translate('languageDocumentationPopover.header', {
- defaultMessage: '{language} reference',
- values: { language },
- })}
+
+
+ {i18n.translate('languageDocumentationPopover.header', {
+ defaultMessage: '{language} reference',
+ values: { language },
+ })}
+
+ {linkToDocumentation && (
+
+
+ {i18n.translate('languageDocumentationPopover.documentationLinkLabel', {
+ defaultMessage: 'View full documentation',
+ })}
+
+
+ )}
+
;
searchInDescription?: boolean;
+ linkToDocumentation?: string;
}
function DocumentationPopover({
@@ -31,6 +32,7 @@ function DocumentationPopover({
sections,
buttonProps,
searchInDescription,
+ linkToDocumentation,
}: DocumentationPopoverProps) {
const [isHelpOpen, setIsHelpOpen] = useState(false);
@@ -71,6 +73,7 @@ function DocumentationPopover({
language={language}
sections={sections}
searchInDescription={searchInDescription}
+ linkToDocumentation={linkToDocumentation}
/>
diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
index 15adf2f293bb8..7bdfce427bc21 100644
--- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
+++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
@@ -163,7 +163,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const language = getAggregateQueryMode(query);
const queryString: string = query[language] ?? '';
const kibana = useKibana();
- const { dataViews, expressions, indexManagementApiService, application } = kibana.services;
+ const { dataViews, expressions, indexManagementApiService, application, docLinks } =
+ kibana.services;
const [code, setCode] = useState(queryString ?? '');
const [codeOneLiner, setCodeOneLiner] = useState('');
// To make server side errors less "sticky", register the state of the code when submitting
@@ -680,6 +681,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
language={getLanguageDisplayName(String(language))}
sections={documentationSections}
searchInDescription
+ linkToDocumentation={
+ language === 'esql' ? docLinks?.links?.query?.queryESQL : ''
+ }
buttonProps={{
color: 'text',
size: 's',
@@ -914,6 +918,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
language={
String(language) === 'esql' ? 'ES|QL' : String(language).toUpperCase()
}
+ linkToDocumentation={
+ language === 'esql' ? docLinks?.links?.query?.queryESQL : ''
+ }
searchInDescription
sections={documentationSections}
buttonProps={{
From cf1ae59ec044c9b80df18c5467a855bc557236cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?=
Date: Thu, 8 Feb 2024 12:14:46 +0100
Subject: [PATCH 010/104] [Obs AI Assistant] Improved logging (#176289)
Tiny PR to improve debug logs
---
.../server/functions/recall.ts | 20 ++++++++++++-------
.../server/service/client/index.ts | 6 ++++++
.../service/knowledge_base_service/index.ts | 8 ++++----
3 files changed, 23 insertions(+), 11 deletions(-)
diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts b/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts
index 7e966fa0e5508..ee0fae1f91ed1 100644
--- a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts
+++ b/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts
@@ -15,6 +15,7 @@ import { FunctionRegistrationParameters } from '.';
import { MessageRole, type Message } from '../../common/types';
import { concatenateChatCompletionChunks } from '../../common/utils/concatenate_chat_completion_chunks';
import type { ObservabilityAIAssistantClient } from '../service/client';
+import { RespondFunctionResources } from '../service/types';
export function registerRecallFunction({
client,
@@ -95,10 +96,6 @@ export function registerRecallFunction({
queries,
});
- resources.logger.debug(`Received ${suggestions.length} suggestions`);
-
- resources.logger.debug(JSON.stringify(suggestions, null, 2));
-
if (suggestions.length === 0) {
return {
content: [] as unknown as Serializable,
@@ -113,11 +110,9 @@ export function registerRecallFunction({
client,
connectorId,
signal,
+ resources,
});
- resources.logger.debug(`Received ${relevantDocuments.length} relevant documents`);
- resources.logger.debug(JSON.stringify(relevantDocuments, null, 2));
-
return {
content: relevantDocuments as unknown as Serializable,
};
@@ -177,6 +172,7 @@ async function scoreSuggestions({
client,
connectorId,
signal,
+ resources,
}: {
suggestions: Awaited>;
systemMessage: Message;
@@ -185,7 +181,10 @@ async function scoreSuggestions({
client: ObservabilityAIAssistantClient;
connectorId: string;
signal: AbortSignal;
+ resources: RespondFunctionResources;
}) {
+ resources.logger.debug(`Suggestions: ${JSON.stringify(suggestions, null, 2)}`);
+
const systemMessageExtension =
dedent(`You have the function called score available to help you inform the user about how relevant you think a given document is to the conversation.
Please give a score between 1 and 7, fractions are allowed.
@@ -262,6 +261,8 @@ async function scoreSuggestions({
scoreFunctionRequest.message.function_call.arguments
);
+ resources.logger.debug(`Scores: ${JSON.stringify(scores, null, 2)}`);
+
if (scores.length === 0) {
return [];
}
@@ -279,5 +280,10 @@ async function scoreSuggestions({
relevantDocumentIds.includes(suggestion.id)
);
+ resources.logger.debug(
+ `Found ${relevantDocumentIds.length} relevant suggestions from the knowledge base. ${scores.length} suggestions were considered in total.`
+ );
+ resources.logger.debug(`Relevant documents: ${JSON.stringify(relevantDocuments, null, 2)}`);
+
return relevantDocuments;
}
diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts
index 76749e75daed1..f3ab3e917979b 100644
--- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts
+++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts
@@ -320,6 +320,10 @@ export class ObservabilityAIAssistantClient {
},
};
+ this.dependencies.logger.debug(
+ `Function response: ${JSON.stringify(functionResponseMessage, null, 2)}`
+ );
+
nextMessages = nextMessages.concat(functionResponseMessage);
subscriber.next({
@@ -357,6 +361,8 @@ export class ObservabilityAIAssistantClient {
return await next(nextMessages);
}
+ this.dependencies.logger.debug(`Conversation: ${JSON.stringify(nextMessages, null, 2)}`);
+
if (!persist) {
subscriber.complete();
return;
diff --git a/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts
index 9442985602e98..6783f972f6b4c 100644
--- a/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts
+++ b/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts
@@ -436,6 +436,7 @@ export class KnowledgeBaseService {
}): Promise<{
entries: RecalledEntry[];
}> => {
+ this.dependencies.logger.debug(`Recalling entries from KB for queries: "${queries}"`);
const modelId = await this.dependencies.getModelId();
const [documentsFromKb, documentsFromConnectors] = await Promise.all([
@@ -482,10 +483,9 @@ export class KnowledgeBaseService {
}
}
- if (returnedEntries.length <= sortedEntries.length) {
- this.dependencies.logger.debug(
- `Dropped ${sortedEntries.length - returnedEntries.length} entries because of token limit`
- );
+ const droppedEntries = sortedEntries.length - returnedEntries.length;
+ if (droppedEntries > 0) {
+ this.dependencies.logger.info(`Dropped ${droppedEntries} entries because of token limit`);
}
return {
From 0f3a72d700d13ff95e33e7f2a862cff411ec8b8c Mon Sep 17 00:00:00 2001
From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
Date: Thu, 8 Feb 2024 12:56:34 +0100
Subject: [PATCH 011/104] [Lens] replace deprecated createReducer object
notation with builder notation (#176404)
We get this message in the console and in our tests:
I updated the syntax to the builder notation.
---------
Co-authored-by: Marco Liberati
---
.../public/state_management/lens_slice.ts | 1597 ++++++++---------
1 file changed, 712 insertions(+), 885 deletions(-)
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts
index 309efeb8ea827..b7b02bdbc934b 100644
--- a/x-pack/plugins/lens/public/state_management/lens_slice.ts
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
+import { createAction, createReducer, current } from '@reduxjs/toolkit';
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import { mapValues, uniq } from 'lodash';
import { Filter, Query } from '@kbn/es-query';
@@ -320,986 +320,813 @@ export const lensActions = {
export const makeLensReducer = (storeDeps: LensStoreDeps) => {
const { datasourceMap, visualizationMap } = storeDeps;
- return createReducer(initialState, {
- [setState.type]: (state, { payload }: PayloadAction>) => {
- return {
- ...state,
- ...payload,
- };
- },
- [setExecutionContext.type]: (state, { payload }: PayloadAction) => {
- return {
- ...state,
- ...payload,
- };
- },
- [initExisting.type]: (state, { payload }: PayloadAction>) => {
- return {
- ...state,
- ...payload,
- };
- },
- [onActiveDataChange.type]: (
- state,
- { payload: { activeData } }: PayloadAction<{ activeData: TableInspectorAdapter }>
- ) => {
- return {
- ...state,
- activeData,
- };
- },
- [setSaveable.type]: (state, { payload }: PayloadAction) => {
- return {
- ...state,
- isSaveable: payload,
- };
- },
- [enableAutoApply.type]: (state) => {
- state.autoApplyDisabled = false;
- },
- [disableAutoApply.type]: (state) => {
- state.autoApplyDisabled = true;
- state.changesApplied = true;
- },
- [applyChanges.type]: (state) => {
- if (typeof state.applyChangesCounter === 'undefined') {
- state.applyChangesCounter = 0;
- }
- state.applyChangesCounter!++;
- },
- [setChangesApplied.type]: (state, { payload: applied }) => {
- state.changesApplied = applied;
- },
- [cloneLayer.type]: (
- state,
- {
- payload: { layerId, newLayerId },
- }: {
- payload: {
- layerId: string;
- newLayerId: string;
+ return createReducer(initialState, (builder) => {
+ builder
+ .addCase(setState, (state, { payload }) => {
+ return {
+ ...state,
+ ...payload,
};
- }
- ) => {
- const clonedIDsMap = new Map();
-
- const getNewId = (prevId: string) => {
- const inMapValue = clonedIDsMap.get(prevId);
- if (!inMapValue) {
- const newId = generateId();
- clonedIDsMap.set(prevId, newId);
- return newId;
+ })
+ .addCase(setExecutionContext, (state, { payload }) => {
+ return {
+ ...state,
+ ...payload,
+ };
+ })
+ .addCase(initExisting, (state, { payload }) => {
+ return {
+ ...state,
+ ...payload,
+ };
+ })
+ .addCase(onActiveDataChange, (state, { payload: { activeData } }) => {
+ return {
+ ...state,
+ activeData,
+ };
+ })
+ .addCase(setSaveable, (state, { payload }) => {
+ return {
+ ...state,
+ isSaveable: payload,
+ };
+ })
+ .addCase(enableAutoApply, (state) => {
+ state.autoApplyDisabled = false;
+ })
+ .addCase(disableAutoApply, (state) => {
+ state.autoApplyDisabled = true;
+ state.changesApplied = true;
+ })
+ .addCase(applyChanges, (state) => {
+ if (typeof state.applyChangesCounter === 'undefined') {
+ state.applyChangesCounter = 0;
}
- return inMapValue;
- };
+ state.applyChangesCounter++;
+ })
+ .addCase(setChangesApplied, (state, { payload: applied }) => {
+ state.changesApplied = applied;
+ })
+ .addCase(cloneLayer, (state, { payload: { layerId, newLayerId } }) => {
+ const clonedIDsMap = new Map();
+
+ const getNewId = (prevId: string) => {
+ const inMapValue = clonedIDsMap.get(prevId);
+ if (!inMapValue) {
+ const newId = generateId();
+ clonedIDsMap.set(prevId, newId);
+ return newId;
+ }
+ return inMapValue;
+ };
- if (!state.activeDatasourceId || !state.visualization.activeId) {
- return state;
- }
+ if (!state.activeDatasourceId || !state.visualization.activeId) {
+ return state;
+ }
- state.datasourceStates = mapValues(state.datasourceStates, (datasourceState, datasourceId) =>
- datasourceId
- ? {
- ...datasourceState,
- state: datasourceMap[datasourceId].cloneLayer(
- datasourceState.state,
- layerId,
- newLayerId,
- getNewId
- ),
- }
- : datasourceState
- );
- state.visualization.state = visualizationMap[state.visualization.activeId].cloneLayer!(
- state.visualization.state,
- layerId,
- newLayerId,
- clonedIDsMap
- );
- },
- [removeOrClearLayer.type]: (
- state,
- {
- payload: { visualizationId, layerId, layerIds },
- }: {
- payload: {
- visualizationId: string;
- layerId: string;
- layerIds: string[];
- };
- }
- ) => {
- const activeVisualization = visualizationMap[visualizationId];
- const activeDataSource = datasourceMap[state.activeDatasourceId!];
- const isOnlyLayer =
- getRemoveOperation(
- activeVisualization,
+ state.datasourceStates = mapValues(
+ state.datasourceStates,
+ (datasourceState, datasourceId) =>
+ datasourceId
+ ? {
+ ...datasourceState,
+ state: datasourceMap[datasourceId].cloneLayer(
+ datasourceState.state,
+ layerId,
+ newLayerId,
+ getNewId
+ ),
+ }
+ : datasourceState
+ );
+ state.visualization.state = visualizationMap[state.visualization.activeId].cloneLayer!(
state.visualization.state,
layerId,
- layerIds.length
- ) === 'clear';
+ newLayerId,
+ clonedIDsMap
+ );
+ })
+ .addCase(removeOrClearLayer, (state, { payload: { visualizationId, layerId, layerIds } }) => {
+ const activeVisualization = visualizationMap[visualizationId];
+ const activeDataSource = datasourceMap[state.activeDatasourceId!];
+ const isOnlyLayer =
+ getRemoveOperation(
+ activeVisualization,
+ state.visualization.state,
+ layerId,
+ layerIds.length
+ ) === 'clear';
- let removedLayerIds: string[] = [];
+ let removedLayerIds: string[] = [];
- state.datasourceStates = mapValues(
- state.datasourceStates,
- (datasourceState, datasourceId) => {
- const datasource = datasourceMap[datasourceId!];
+ state.datasourceStates = mapValues(
+ state.datasourceStates,
+ (datasourceState, datasourceId) => {
+ const datasource = datasourceMap[datasourceId!];
- const { newState, removedLayerIds: removedLayerIdsForThisDatasource } = isOnlyLayer
- ? datasource.clearLayer(datasourceState.state, layerId)
- : datasource.removeLayer(datasourceState.state, layerId);
+ const { newState, removedLayerIds: removedLayerIdsForThisDatasource } = isOnlyLayer
+ ? datasource.clearLayer(datasourceState.state, layerId)
+ : datasource.removeLayer(datasourceState.state, layerId);
- removedLayerIds = [...removedLayerIds, ...removedLayerIdsForThisDatasource];
+ removedLayerIds = [...removedLayerIds, ...removedLayerIdsForThisDatasource];
- return {
- ...datasourceState,
- ...(datasourceId === state.activeDatasourceId && {
- state: newState,
- }),
- };
- }
- );
- state.stagedPreview = undefined;
- // reuse the activeDatasource current dataView id for the moment
- const currentDataViewsId = activeDataSource.getUsedDataView(
- state.datasourceStates[state.activeDatasourceId!].state
- );
-
- if (isOnlyLayer || !activeVisualization.removeLayer) {
- state.visualization.state = activeVisualization.clearLayer(
- state.visualization.state,
- layerId,
- currentDataViewsId
+ return {
+ ...datasourceState,
+ ...(datasourceId === state.activeDatasourceId && {
+ state: newState,
+ }),
+ };
+ }
+ );
+ state.stagedPreview = undefined;
+ // reuse the activeDatasource current dataView id for the moment
+ const currentDataViewsId = activeDataSource.getUsedDataView(
+ state.datasourceStates[state.activeDatasourceId!].state
);
- }
- uniq(removedLayerIds).forEach(
- (removedId) =>
- (state.visualization.state = activeVisualization.removeLayer?.(
+ if (isOnlyLayer || !activeVisualization.removeLayer) {
+ state.visualization.state = activeVisualization.clearLayer(
state.visualization.state,
- removedId
- ))
- );
- },
- [changeIndexPattern.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- visualizationIds?: string;
- datasourceIds?: string;
- layerId?: string;
- indexPatternId: string;
- dataViews: Pick;
- };
- }
- ) => {
- const { visualizationIds, datasourceIds, layerId, indexPatternId, dataViews } = payload;
- const newIndexPatternRefs = [...state.dataViews.indexPatternRefs];
- const availableRefs = new Set(newIndexPatternRefs.map((ref) => ref.id));
- // check for missing refs
- Object.values(dataViews.indexPatterns || {}).forEach((indexPattern) => {
- if (!availableRefs.has(indexPattern.id)) {
- newIndexPatternRefs.push({
- id: indexPattern.id!,
- name: indexPattern.name,
- title: indexPattern.title,
- });
- }
- });
- const newState: Partial = {
- dataViews: {
- ...state.dataViews,
- indexPatterns: dataViews.indexPatterns,
- indexPatternRefs: newIndexPatternRefs,
- },
- };
- if (visualizationIds?.length) {
- for (const visualizationId of visualizationIds) {
- const activeVisualization =
- visualizationId &&
- state.visualization.activeId === visualizationId &&
- visualizationMap[visualizationId];
- if (activeVisualization && layerId && activeVisualization?.onIndexPatternChange) {
- newState.visualization = {
- ...state.visualization,
- state: activeVisualization.onIndexPatternChange(
- state.visualization.state,
- indexPatternId,
- layerId
- ),
- };
- }
+ layerId,
+ currentDataViewsId
+ );
}
- }
- if (datasourceIds?.length) {
- newState.datasourceStates = { ...state.datasourceStates };
- const frame = selectFramePublicAPI(
- { lens: { ...current(state), dataViews: newState.dataViews! } },
- datasourceMap
- );
- const datasourceLayers = frame.datasourceLayers;
- for (const datasourceId of datasourceIds) {
- const activeDatasource = datasourceId && datasourceMap[datasourceId];
- if (activeDatasource && activeDatasource?.onIndexPatternChange) {
- newState.datasourceStates = {
- ...newState.datasourceStates,
- [datasourceId]: {
- isLoading: false,
- state: activeDatasource.onIndexPatternChange(
- newState.datasourceStates[datasourceId].state,
- dataViews.indexPatterns,
+ uniq(removedLayerIds).forEach(
+ (removedId) =>
+ (state.visualization.state = activeVisualization.removeLayer?.(
+ state.visualization.state,
+ removedId
+ ))
+ );
+ })
+ .addCase(changeIndexPattern, (state, { payload }) => {
+ const { visualizationIds, datasourceIds, layerId, indexPatternId, dataViews } = payload;
+ if (!dataViews.indexPatterns) {
+ throw new Error('Invariant: indexPatterns should be defined');
+ }
+ const newIndexPatternRefs = [...state.dataViews.indexPatternRefs];
+ const availableRefs = new Set(newIndexPatternRefs.map((ref) => ref.id));
+ // check for missing refs
+ Object.values(dataViews.indexPatterns || {}).forEach((indexPattern) => {
+ if (!availableRefs.has(indexPattern.id)) {
+ newIndexPatternRefs.push({
+ id: indexPattern.id!,
+ name: indexPattern.name,
+ title: indexPattern.title,
+ });
+ }
+ });
+ const newState: Partial = {
+ dataViews: {
+ ...state.dataViews,
+ indexPatterns: dataViews.indexPatterns,
+ indexPatternRefs: newIndexPatternRefs,
+ },
+ };
+ if (visualizationIds?.length) {
+ for (const visualizationId of visualizationIds) {
+ const activeVisualization =
+ visualizationId &&
+ state.visualization.activeId === visualizationId &&
+ visualizationMap[visualizationId];
+ if (activeVisualization && layerId && activeVisualization?.onIndexPatternChange) {
+ newState.visualization = {
+ ...state.visualization,
+ state: activeVisualization.onIndexPatternChange(
+ state.visualization.state,
indexPatternId,
layerId
),
- },
- };
- // Update the visualization columns
- if (layerId && state.visualization.activeId) {
- const nextPublicAPI = activeDatasource.getPublicAPI({
- state: newState.datasourceStates[datasourceId].state,
- layerId,
- indexPatterns: dataViews.indexPatterns,
- });
- const nextTable = new Set(
- nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
- );
- const datasourcePublicAPI = datasourceLayers[layerId];
- if (datasourcePublicAPI) {
- const removed = datasourcePublicAPI
- .getTableSpec()
- .map(({ columnId }) => columnId)
- .filter((columnId) => !nextTable.has(columnId));
- const activeVisualization = visualizationMap[state.visualization.activeId];
- let nextVisState = (newState.visualization || state.visualization).state;
- removed.forEach((columnId) => {
- nextVisState = activeVisualization.removeDimension({
- layerId,
- columnId,
- prevState: nextVisState,
- frame,
- });
+ };
+ }
+ }
+ }
+ if (datasourceIds?.length) {
+ newState.datasourceStates = { ...state.datasourceStates };
+ const frame = selectFramePublicAPI(
+ { lens: { ...current(state), dataViews: newState.dataViews! } },
+ datasourceMap
+ );
+ const datasourceLayers = frame.datasourceLayers;
+
+ for (const datasourceId of datasourceIds) {
+ const activeDatasource = datasourceId && datasourceMap[datasourceId];
+ if (activeDatasource && activeDatasource?.onIndexPatternChange) {
+ newState.datasourceStates = {
+ ...newState.datasourceStates,
+ [datasourceId]: {
+ isLoading: false,
+ state: activeDatasource.onIndexPatternChange(
+ newState.datasourceStates[datasourceId].state,
+ dataViews.indexPatterns,
+ indexPatternId,
+ layerId
+ ),
+ },
+ };
+ // Update the visualization columns
+ if (layerId && state.visualization.activeId) {
+ const nextPublicAPI = activeDatasource.getPublicAPI({
+ state: newState.datasourceStates[datasourceId].state,
+ layerId,
+ indexPatterns: dataViews.indexPatterns,
});
- newState.visualization = {
- ...state.visualization,
- state: nextVisState,
- };
+ const nextTable = new Set(
+ nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
+ );
+ const datasourcePublicAPI = datasourceLayers[layerId];
+ if (datasourcePublicAPI) {
+ const removed = datasourcePublicAPI
+ .getTableSpec()
+ .map(({ columnId }) => columnId)
+ .filter((columnId) => !nextTable.has(columnId));
+ const activeVisualization = visualizationMap[state.visualization.activeId];
+ let nextVisState = (newState.visualization || state.visualization).state;
+ removed.forEach((columnId) => {
+ nextVisState = activeVisualization.removeDimension({
+ layerId,
+ columnId,
+ prevState: nextVisState,
+ frame,
+ });
+ });
+ newState.visualization = {
+ ...state.visualization,
+ state: nextVisState,
+ };
+ }
}
}
}
}
- }
- return { ...state, ...newState };
- },
- [updateIndexPatterns.type]: (state, { payload }: { payload: Partial }) => {
- return {
- ...state,
- dataViews: { ...state.dataViews, ...payload },
- };
- },
- [replaceIndexpattern.type]: (
- state,
- { payload }: { payload: { newIndexPattern: IndexPattern; oldId: string } }
- ) => {
- state.dataViews.indexPatterns[payload.newIndexPattern.id] = payload.newIndexPattern;
- delete state.dataViews.indexPatterns[payload.oldId];
- state.dataViews.indexPatternRefs = state.dataViews.indexPatternRefs.filter(
- (r) => r.id !== payload.oldId
- );
- state.dataViews.indexPatternRefs.push({
- id: payload.newIndexPattern.id,
- title: payload.newIndexPattern.title,
- name: payload.newIndexPattern.name,
- });
- const visualization = visualizationMap[state.visualization.activeId!];
- state.visualization.state =
- visualization.onIndexPatternRename?.(
- state.visualization.state,
- payload.oldId,
- payload.newIndexPattern.id
- ) ?? state.visualization.state;
-
- Object.entries(state.datasourceStates).forEach(([datasourceId, datasourceState]) => {
- const datasource = datasourceMap[datasourceId];
- state.datasourceStates[datasourceId].state =
- datasource?.onIndexPatternRename?.(
- datasourceState.state,
- payload.oldId,
- payload.newIndexPattern.id!
- ) ?? datasourceState.state;
- });
- },
- [updateDatasourceState.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- newDatasourceState: unknown;
- datasourceId: string;
- clearStagedPreview?: boolean;
- dontSyncLinkedDimensions: boolean;
+ return { ...state, ...newState };
+ })
+ .addCase(updateIndexPatterns, (state, { payload }) => {
+ return {
+ ...state,
+ dataViews: { ...state.dataViews, ...payload },
};
- }
- ) => {
- if (payload.clearStagedPreview) {
- state.stagedPreview = undefined;
- }
+ })
+ .addCase(replaceIndexpattern, (state, { payload }) => {
+ state.dataViews.indexPatterns[payload.newIndexPattern.id] = payload.newIndexPattern;
+ delete state.dataViews.indexPatterns[payload.oldId];
+ state.dataViews.indexPatternRefs = state.dataViews.indexPatternRefs.filter(
+ (r) => r.id !== payload.oldId
+ );
+ state.dataViews.indexPatternRefs.push({
+ id: payload.newIndexPattern.id,
+ title: payload.newIndexPattern.title,
+ name: payload.newIndexPattern.name,
+ });
+ const visualization = visualizationMap[state.visualization.activeId!];
+ state.visualization.state =
+ visualization.onIndexPatternRename?.(
+ state.visualization.state,
+ payload.oldId,
+ payload.newIndexPattern.id
+ ) ?? state.visualization.state;
+
+ Object.entries(state.datasourceStates).forEach(([datasourceId, datasourceState]) => {
+ const datasource = datasourceMap[datasourceId];
+ state.datasourceStates[datasourceId].state =
+ datasource?.onIndexPatternRename?.(
+ datasourceState.state,
+ payload.oldId,
+ payload.newIndexPattern.id!
+ ) ?? datasourceState.state;
+ });
+ })
+ .addCase(updateDatasourceState, (state, { payload }) => {
+ if (payload.clearStagedPreview) {
+ state.stagedPreview = undefined;
+ }
- state.datasourceStates[payload.datasourceId] = {
- state: payload.newDatasourceState,
- isLoading: false,
- };
+ state.datasourceStates[payload.datasourceId] = {
+ state: payload.newDatasourceState,
+ isLoading: false,
+ };
- if (payload.dontSyncLinkedDimensions) {
- return;
- }
+ if (payload.dontSyncLinkedDimensions) {
+ return;
+ }
- const currentState = current(state);
+ const currentState = current(state);
- const {
- datasourceState: syncedDatasourceState,
- visualizationState: syncedVisualizationState,
- } = syncLinkedDimensions(currentState, visualizationMap, datasourceMap, payload.datasourceId);
+ const {
+ datasourceState: syncedDatasourceState,
+ visualizationState: syncedVisualizationState,
+ } = syncLinkedDimensions(
+ currentState,
+ visualizationMap,
+ datasourceMap,
+ payload.datasourceId
+ );
- state.visualization.state = syncedVisualizationState;
- state.datasourceStates[payload.datasourceId].state = syncedDatasourceState;
- },
- [updateVisualizationState.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- visualizationId: string;
- newState: unknown;
- dontSyncLinkedDimensions?: boolean;
- };
- }
- ) => {
- if (!state.visualization.activeId) {
- throw new Error('Invariant: visualization state got updated without active visualization');
- }
- // This is a safeguard that prevents us from accidentally updating the
- // wrong visualization. This occurs in some cases due to the uncoordinated
- // way we manage state across plugins.
- if (state.visualization.activeId !== payload.visualizationId) {
- return state;
- }
+ state.visualization.state = syncedVisualizationState;
+ state.datasourceStates[payload.datasourceId].state = syncedDatasourceState;
+ })
+ .addCase(updateVisualizationState, (state, { payload }) => {
+ if (!state.visualization.activeId) {
+ throw new Error(
+ 'Invariant: visualization state got updated without active visualization'
+ );
+ }
+ // This is a safeguard that prevents us from accidentally updating the
+ // wrong visualization. This occurs in some cases due to the uncoordinated
+ // way we manage state across plugins.
+ if (state.visualization.activeId !== payload.visualizationId) {
+ return state;
+ }
- state.visualization.state = payload.newState;
+ state.visualization.state = payload.newState;
- if (!state.activeDatasourceId) {
- return;
- }
+ if (!state.activeDatasourceId) {
+ return;
+ }
- if (payload.dontSyncLinkedDimensions) {
- return;
- }
+ if (payload.dontSyncLinkedDimensions) {
+ return;
+ }
- // TODO - consolidate into applySyncLinkedDimensions
- const {
- datasourceState: syncedDatasourceState,
- visualizationState: syncedVisualizationState,
- } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
+ // TODO - consolidate into applySyncLinkedDimensions
+ const {
+ datasourceState: syncedDatasourceState,
+ visualizationState: syncedVisualizationState,
+ } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
- state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState;
- state.visualization.state = syncedVisualizationState;
- },
+ state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState;
+ state.visualization.state = syncedVisualizationState;
+ })
- [switchVisualization.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- suggestion: {
- newVisualizationId: string;
- visualizationState: unknown;
- datasourceState?: unknown;
- datasourceId?: string;
- };
- clearStagedPreview?: boolean;
- };
- }
- ) => {
- const { newVisualizationId, visualizationState, datasourceState, datasourceId } =
- payload.suggestion;
- return {
- ...state,
- datasourceStates: datasourceId
- ? {
- ...state.datasourceStates,
- [datasourceId]: {
- ...state.datasourceStates[datasourceId],
- state: datasourceState,
+ .addCase(switchVisualization, (state, { payload }) => {
+ const { newVisualizationId, visualizationState, datasourceState, datasourceId } =
+ payload.suggestion;
+ return {
+ ...state,
+ datasourceStates: datasourceId
+ ? {
+ ...state.datasourceStates,
+ [datasourceId]: {
+ ...state.datasourceStates[datasourceId],
+ state: datasourceState,
+ },
+ }
+ : state.datasourceStates,
+ visualization: {
+ ...state.visualization,
+ activeId: newVisualizationId,
+ state: visualizationState,
+ },
+ stagedPreview: payload.clearStagedPreview
+ ? undefined
+ : state.stagedPreview || {
+ datasourceStates: state.datasourceStates,
+ visualization: state.visualization,
+ activeData: state.activeData,
},
- }
- : state.datasourceStates,
- visualization: {
- ...state.visualization,
- activeId: newVisualizationId,
- state: visualizationState,
- },
- stagedPreview: payload.clearStagedPreview
- ? undefined
- : state.stagedPreview || {
- datasourceStates: state.datasourceStates,
- visualization: state.visualization,
- activeData: state.activeData,
- },
- };
- },
- [rollbackSuggestion.type]: (state) => {
- return {
- ...state,
- ...(state.stagedPreview || {}),
- stagedPreview: undefined,
- };
- },
- [setToggleFullscreen.type]: (state) => {
- return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource };
- },
- [submitSuggestion.type]: (state) => {
- return {
- ...state,
- stagedPreview: undefined,
- };
- },
- [switchDatasource.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- newDatasourceId: string;
};
- }
- ) => {
- return {
- ...state,
- datasourceStates: {
- ...state.datasourceStates,
- [payload.newDatasourceId]: state.datasourceStates[payload.newDatasourceId] || {
- state: null,
- isLoading: true,
+ })
+ .addCase(rollbackSuggestion, (state) => {
+ return {
+ ...state,
+ ...(state.stagedPreview || {}),
+ stagedPreview: undefined,
+ };
+ })
+ .addCase(setToggleFullscreen, (state) => {
+ return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource };
+ })
+ .addCase(submitSuggestion, (state) => {
+ return {
+ ...state,
+ stagedPreview: undefined,
+ };
+ })
+ .addCase(switchDatasource, (state, { payload }) => {
+ return {
+ ...state,
+ datasourceStates: {
+ ...state.datasourceStates,
+ [payload.newDatasourceId]: state.datasourceStates[payload.newDatasourceId] || {
+ state: null,
+ isLoading: true,
+ },
},
- },
- activeDatasourceId: payload.newDatasourceId,
- };
- },
- [switchAndCleanDatasource.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- newDatasourceId: string;
- visualizationId?: string;
- currentIndexPatternId?: string;
+ activeDatasourceId: payload.newDatasourceId,
};
- }
- ) => {
- const activeVisualization =
- payload.visualizationId && visualizationMap[payload.visualizationId];
- const visualization = state.visualization;
- let newVizState = visualization.state;
- const ids: string[] = [];
- if (activeVisualization && activeVisualization.getLayerIds) {
- const layerIds = activeVisualization.getLayerIds(visualization.state);
- ids.push(...Object.values(layerIds));
- newVizState = activeVisualization.initialize(() => ids[0]);
- }
- const currentVizId = ids[0];
+ })
+ .addCase(switchAndCleanDatasource, (state, { payload }) => {
+ const activeVisualization =
+ payload.visualizationId && visualizationMap[payload.visualizationId];
+ const visualization = state.visualization;
+ let newVizState = visualization.state;
+ const ids: string[] = [];
+ if (activeVisualization && activeVisualization.getLayerIds) {
+ const layerIds = activeVisualization.getLayerIds(visualization.state);
+ ids.push(...Object.values(layerIds));
+ newVizState = activeVisualization.initialize(() => ids[0]);
+ }
+ const currentVizId = ids[0];
- const datasourceState = current(state).datasourceStates[payload.newDatasourceId]
- ? current(state).datasourceStates[payload.newDatasourceId]?.state
- : datasourceMap[payload.newDatasourceId].createEmptyLayer(
- payload.currentIndexPatternId ?? ''
- );
- const updatedState = datasourceMap[payload.newDatasourceId].insertLayer(
- datasourceState,
- currentVizId
- );
+ const datasourceState = current(state).datasourceStates[payload.newDatasourceId]
+ ? current(state).datasourceStates[payload.newDatasourceId]?.state
+ : datasourceMap[payload.newDatasourceId].createEmptyLayer(
+ payload.currentIndexPatternId ?? ''
+ );
+ const updatedState = datasourceMap[payload.newDatasourceId].insertLayer(
+ datasourceState,
+ currentVizId
+ );
- return {
- ...state,
- datasourceStates: {
- [payload.newDatasourceId]: {
- state: updatedState,
- isLoading: false,
- },
- },
- activeDatasourceId: payload.newDatasourceId,
- visualization: {
- ...visualization,
- state: newVizState,
- },
- };
- },
- [navigateAway.type]: (state) => state,
- [loadInitial.type]: (
- state,
- payload: PayloadAction<{
- initialInput?: LensEmbeddableInput;
- redirectCallback?: (savedObjectId?: string) => void;
- history?: History;
- inlineEditing?: boolean;
- }>
- ) => state,
- [initEmpty.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- newState: Partial;
- initialContext: VisualizeFieldContext | VisualizeEditorContext | undefined;
- layerId: string;
- };
- }
- ) => {
- const newState = {
- ...state,
- ...payload.newState,
- };
- const suggestion: Suggestion | undefined = getVisualizeFieldSuggestions({
- datasourceMap,
- datasourceStates: newState.datasourceStates,
- visualizationMap,
- visualizeTriggerFieldContext: payload.initialContext,
- dataViews: newState.dataViews,
- });
- if (suggestion) {
return {
- ...newState,
+ ...state,
datasourceStates: {
- ...newState.datasourceStates,
- [suggestion.datasourceId!]: {
- ...newState.datasourceStates[suggestion.datasourceId!],
- state: suggestion.datasourceState,
+ [payload.newDatasourceId]: {
+ state: updatedState,
+ isLoading: false,
},
},
+ activeDatasourceId: payload.newDatasourceId,
visualization: {
- ...newState.visualization,
- activeId: suggestion.visualizationId,
- state: suggestion.visualizationState,
+ ...visualization,
+ state: newVizState,
},
- stagedPreview: undefined,
};
- }
+ })
+ .addCase(navigateAway, (state) => state)
+ .addCase(loadInitial, (state, payload) => state)
+ .addCase(initEmpty, (state, { payload }) => {
+ const newState = {
+ ...state,
+ ...payload.newState,
+ };
+ const suggestion: Suggestion | undefined = getVisualizeFieldSuggestions({
+ datasourceMap,
+ datasourceStates: newState.datasourceStates,
+ visualizationMap,
+ visualizeTriggerFieldContext: payload.initialContext,
+ dataViews: newState.dataViews,
+ });
+ if (suggestion) {
+ return {
+ ...newState,
+ datasourceStates: {
+ ...newState.datasourceStates,
+ [suggestion.datasourceId!]: {
+ ...newState.datasourceStates[suggestion.datasourceId!],
+ state: suggestion.datasourceState,
+ },
+ },
+ visualization: {
+ ...newState.visualization,
+ activeId: suggestion.visualizationId,
+ state: suggestion.visualizationState,
+ },
+ stagedPreview: undefined,
+ };
+ }
- const visualization = newState.visualization;
+ const visualization = newState.visualization;
- if (!visualization.activeId) {
- throw new Error('Invariant: visualization state got updated without active visualization');
- }
+ if (!visualization.activeId) {
+ throw new Error(
+ 'Invariant: visualization state got updated without active visualization'
+ );
+ }
- const activeVisualization = visualizationMap[visualization.activeId];
- if (visualization.state === null && activeVisualization) {
- const activeDatasourceId = getInitialDatasourceId(datasourceMap)!;
- const newVisState = activeVisualization.initialize(() => payload.layerId);
- const activeDatasource = datasourceMap[activeDatasourceId];
+ const activeVisualization = visualizationMap[visualization.activeId];
+ if (visualization.state === null && activeVisualization) {
+ const activeDatasourceId = getInitialDatasourceId(datasourceMap)!;
+ const newVisState = activeVisualization.initialize(() => payload.layerId);
+ const activeDatasource = datasourceMap[activeDatasourceId];
+ return {
+ ...newState,
+ activeDatasourceId,
+ datasourceStates: {
+ ...newState.datasourceStates,
+ [activeDatasourceId]: {
+ ...newState.datasourceStates[activeDatasourceId],
+ state: activeDatasource.insertLayer(
+ newState.datasourceStates[activeDatasourceId]?.state,
+ payload.layerId
+ ),
+ },
+ },
+ visualization: {
+ ...visualization,
+ state: newVisState,
+ },
+ };
+ }
+ return newState;
+ })
+ .addCase(editVisualizationAction, (state, { payload }) => {
+ if (!state.visualization.activeId) {
+ throw new Error(
+ 'Invariant: visualization state got updated without active visualization'
+ );
+ }
+ // This is a safeguard that prevents us from accidentally updating the
+ // wrong visualization. This occurs in some cases due to the uncoordinated
+ // way we manage state across plugins.
+ if (state.visualization.activeId !== payload.visualizationId) {
+ return state;
+ }
+ const activeVisualization = visualizationMap[payload.visualizationId];
+ if (activeVisualization?.onEditAction) {
+ state.visualization.state = activeVisualization.onEditAction(
+ state.visualization.state,
+ payload.event
+ );
+ }
+ })
+ .addCase(insertLayer, (state, { payload }) => {
+ const updater = datasourceMap[payload.datasourceId].insertLayer;
return {
- ...newState,
- activeDatasourceId,
+ ...state,
datasourceStates: {
- ...newState.datasourceStates,
- [activeDatasourceId]: {
- ...newState.datasourceStates[activeDatasourceId],
- state: activeDatasource.insertLayer(
- newState.datasourceStates[activeDatasourceId]?.state,
+ ...state.datasourceStates,
+ [payload.datasourceId]: {
+ ...state.datasourceStates[payload.datasourceId],
+ state: updater(
+ current(state).datasourceStates[payload.datasourceId].state,
payload.layerId
),
},
},
- visualization: {
- ...visualization,
- state: newVisState,
- },
- };
- }
- return newState;
- },
- [editVisualizationAction.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- visualizationId: string;
- event: LensEditEvent;
};
- }
- ) => {
- if (!state.visualization.activeId) {
- throw new Error('Invariant: visualization state got updated without active visualization');
- }
- // This is a safeguard that prevents us from accidentally updating the
- // wrong visualization. This occurs in some cases due to the uncoordinated
- // way we manage state across plugins.
- if (state.visualization.activeId !== payload.visualizationId) {
- return state;
- }
- const activeVisualization = visualizationMap[payload.visualizationId];
- if (activeVisualization?.onEditAction) {
- state.visualization.state = activeVisualization.onEditAction(
- state.visualization.state,
- payload.event
- );
- }
- },
- [insertLayer.type]: (
- state,
- {
- payload,
- }: {
- payload: {
- layerId: string;
- datasourceId: string;
- };
- }
- ) => {
- const updater = datasourceMap[payload.datasourceId].insertLayer;
- return {
- ...state,
- datasourceStates: {
- ...state.datasourceStates,
- [payload.datasourceId]: {
- ...state.datasourceStates[payload.datasourceId],
- state: updater(
- current(state).datasourceStates[payload.datasourceId].state,
- payload.layerId
- ),
- },
- },
- };
- },
- [removeLayers.type]: (
- state,
- {
- payload: { visualizationId, layerIds },
- }: {
- payload: {
- visualizationId: VisualizationState['activeId'];
- layerIds: string[];
- };
- }
- ) => {
- if (!state.visualization.activeId) {
- throw new Error('Invariant: visualization state got updated without active visualization');
- }
-
- const activeVisualization = visualizationId && visualizationMap[visualizationId];
-
- // This is a safeguard that prevents us from accidentally updating the
- // wrong visualization. This occurs in some cases due to the uncoordinated
- // way we manage state across plugins.
- if (
- state.visualization.activeId === visualizationId &&
- activeVisualization &&
- activeVisualization.removeLayer &&
- state.visualization.state
- ) {
- const updater = layerIds.reduce(
- (acc, layerId) =>
- activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc,
- state.visualization.state
- );
+ })
+ .addCase(removeLayers, (state, { payload: { visualizationId, layerIds } }) => {
+ if (!state.visualization.activeId) {
+ throw new Error(
+ 'Invariant: visualization state got updated without active visualization'
+ );
+ }
- state.visualization.state =
- typeof updater === 'function' ? updater(current(state.visualization.state)) : updater;
- }
+ const activeVisualization = visualizationId && visualizationMap[visualizationId];
- layerIds.forEach((layerId) => {
- const [layerDatasourceId] =
- Object.entries(datasourceMap).find(([datasourceId, datasource]) => {
- return (
- state.datasourceStates[datasourceId] &&
- datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId)
- );
- }) ?? [];
- if (layerDatasourceId) {
- const { newState } = datasourceMap[layerDatasourceId].removeLayer(
- current(state).datasourceStates[layerDatasourceId].state,
- layerId
+ // This is a safeguard that prevents us from accidentally updating the
+ // wrong visualization. This occurs in some cases due to the uncoordinated
+ // way we manage state across plugins.
+ if (
+ state.visualization.activeId === visualizationId &&
+ activeVisualization &&
+ activeVisualization.removeLayer &&
+ state.visualization.state
+ ) {
+ const updater = layerIds.reduce(
+ (acc, layerId) =>
+ activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc,
+ state.visualization.state
);
- state.datasourceStates[layerDatasourceId].state = newState;
- // TODO - call removeLayer for any extra (linked) layers removed by the datasource
- }
- });
- },
- [addLayer.type]: (
- state,
- {
- payload: { layerId, layerType, extraArg, ignoreInitialValues },
- }: {
- payload: {
- layerId: string;
- layerType: LayerType;
- extraArg: unknown;
- ignoreInitialValues: boolean;
- };
- }
- ) => {
- if (!state.activeDatasourceId || !state.visualization.activeId) {
- return state;
- }
+ state.visualization.state =
+ typeof updater === 'function' ? updater(current(state.visualization.state)) : updater;
+ }
- const activeVisualization = visualizationMap[state.visualization.activeId];
- const activeDatasource = datasourceMap[state.activeDatasourceId];
- // reuse the active datasource dataView id for the new layer
- const currentDataViewsId = activeDatasource.getUsedDataView(
- state.datasourceStates[state.activeDatasourceId!].state
- );
- const visualizationState = activeVisualization.appendLayer!(
- state.visualization.state,
- layerId,
- layerType,
- currentDataViewsId,
- extraArg
- );
+ layerIds.forEach((layerId) => {
+ const [layerDatasourceId] =
+ Object.entries(datasourceMap).find(([datasourceId, datasource]) => {
+ return (
+ state.datasourceStates[datasourceId] &&
+ datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId)
+ );
+ }) ?? [];
+ if (layerDatasourceId) {
+ const { newState } = datasourceMap[layerDatasourceId].removeLayer(
+ current(state).datasourceStates[layerDatasourceId].state,
+ layerId
+ );
+ state.datasourceStates[layerDatasourceId].state = newState;
+ // TODO - call removeLayer for any extra (linked) layers removed by the datasource
+ }
+ });
+ })
- const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
+ .addCase(
+ addLayer,
+ (state, { payload: { layerId, layerType, extraArg, ignoreInitialValues } }) => {
+ if (!state.activeDatasourceId || !state.visualization.activeId) {
+ return state;
+ }
- const { noDatasource } =
- activeVisualization
- .getSupportedLayers(visualizationState, framePublicAPI)
- .find(({ type }) => type === layerType) || {};
-
- const layersToLinkTo =
- activeVisualization.getLayersToLinkTo?.(visualizationState, layerId) ?? [];
-
- const datasourceState =
- !noDatasource && activeDatasource
- ? activeDatasource.insertLayer(
- state.datasourceStates[state.activeDatasourceId].state,
- layerId,
- layersToLinkTo
- )
- : state.datasourceStates[state.activeDatasourceId].state;
-
- const { activeDatasourceState, activeVisualizationState } = ignoreInitialValues
- ? { activeDatasourceState: datasourceState, activeVisualizationState: visualizationState }
- : addInitialValueIfAvailable({
- datasourceState,
- visualizationState,
- framePublicAPI,
- activeVisualization,
- activeDatasource,
+ const activeVisualization = visualizationMap[state.visualization.activeId];
+ const activeDatasource = datasourceMap[state.activeDatasourceId];
+ // reuse the active datasource dataView id for the new layer
+ const currentDataViewsId = activeDatasource.getUsedDataView(
+ state.datasourceStates[state.activeDatasourceId!].state
+ );
+ const visualizationState = activeVisualization.appendLayer!(
+ state.visualization.state,
layerId,
layerType,
- });
+ currentDataViewsId,
+ extraArg
+ );
- state.visualization.state = activeVisualizationState;
- state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState;
- state.stagedPreview = undefined;
+ const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
+
+ const { noDatasource } =
+ activeVisualization
+ .getSupportedLayers(visualizationState, framePublicAPI)
+ .find(({ type }) => type === layerType) || {};
+
+ const layersToLinkTo =
+ activeVisualization.getLayersToLinkTo?.(visualizationState, layerId) ?? [];
+
+ const datasourceState =
+ !noDatasource && activeDatasource
+ ? activeDatasource.insertLayer(
+ state.datasourceStates[state.activeDatasourceId].state,
+ layerId,
+ layersToLinkTo
+ )
+ : state.datasourceStates[state.activeDatasourceId].state;
+
+ const { activeDatasourceState, activeVisualizationState } = ignoreInitialValues
+ ? {
+ activeDatasourceState: datasourceState,
+ activeVisualizationState: visualizationState,
+ }
+ : addInitialValueIfAvailable({
+ datasourceState,
+ visualizationState,
+ framePublicAPI,
+ activeVisualization,
+ activeDatasource,
+ layerId,
+ layerType,
+ });
- const {
- datasourceState: syncedDatasourceState,
- visualizationState: syncedVisualizationState,
- } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
+ state.visualization.state = activeVisualizationState;
+ state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState;
+ state.stagedPreview = undefined;
- state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState;
- state.visualization.state = syncedVisualizationState;
- },
- [onDropToDimension.type]: (
- state,
- {
- payload: { source, target, dropType },
- }: {
- payload: {
- source: DragDropIdentifier;
- target: DragDropOperation;
- dropType: DropType;
- };
- }
- ) => {
- if (!state.visualization.activeId) {
- return state;
- }
+ const {
+ datasourceState: syncedDatasourceState,
+ visualizationState: syncedVisualizationState,
+ } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
- const activeVisualization = visualizationMap[state.visualization.activeId];
- const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
+ state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState;
+ state.visualization.state = syncedVisualizationState;
+ }
+ )
+ .addCase(onDropToDimension, (state, { payload: { source, target, dropType } }) => {
+ if (!state.visualization.activeId) {
+ return state;
+ }
- const { groups } = activeVisualization.getConfiguration({
- layerId: target.layerId,
- frame: framePublicAPI,
- state: state.visualization.state,
- });
+ const activeVisualization = visualizationMap[state.visualization.activeId];
+ const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
- const [layerDatasourceId, layerDatasource] =
- Object.entries(datasourceMap).find(
- ([datasourceId, datasource]) =>
- state.datasourceStates[datasourceId] &&
- datasource
- .getLayers(state.datasourceStates[datasourceId].state)
- .includes(target.layerId)
- ) || [];
-
- let newDatasourceState;
-
- if (layerDatasource && layerDatasourceId) {
- newDatasourceState = layerDatasource?.onDrop({
- state: state.datasourceStates[layerDatasourceId].state,
- source,
- target: {
- ...(target as unknown as DragDropOperation),
- filterOperations:
- groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations ||
- Boolean,
- },
- targetLayerDimensionGroups: groups,
- dropType,
- indexPatterns: framePublicAPI.dataViews.indexPatterns,
+ const { groups } = activeVisualization.getConfiguration({
+ layerId: target.layerId,
+ frame: framePublicAPI,
+ state: state.visualization.state,
});
- if (!newDatasourceState) {
- return;
+
+ const [layerDatasourceId, layerDatasource] =
+ Object.entries(datasourceMap).find(
+ ([datasourceId, datasource]) =>
+ state.datasourceStates[datasourceId] &&
+ datasource
+ .getLayers(state.datasourceStates[datasourceId].state)
+ .includes(target.layerId)
+ ) || [];
+
+ let newDatasourceState;
+
+ if (layerDatasource && layerDatasourceId) {
+ newDatasourceState = layerDatasource?.onDrop({
+ state: state.datasourceStates[layerDatasourceId].state,
+ source,
+ target: {
+ ...(target as unknown as DragDropOperation),
+ filterOperations:
+ groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations ||
+ Boolean,
+ },
+ targetLayerDimensionGroups: groups,
+ dropType,
+ indexPatterns: framePublicAPI.dataViews.indexPatterns,
+ });
+ if (!newDatasourceState) {
+ return;
+ }
+ state.datasourceStates[layerDatasourceId].state = newDatasourceState;
}
- state.datasourceStates[layerDatasourceId].state = newDatasourceState;
- }
- activeVisualization.onDrop = activeVisualization.onDrop?.bind(activeVisualization);
- const newVisualizationState = (activeVisualization.onDrop || onDropForVisualization)?.(
- {
- prevState: state.visualization.state,
- frame: framePublicAPI,
- target,
- source,
- dropType,
- group: groups.find(({ groupId: gId }) => gId === target.groupId),
- },
- activeVisualization
- );
- state.visualization.state = newVisualizationState;
+ activeVisualization.onDrop = activeVisualization.onDrop?.bind(activeVisualization);
+ const newVisualizationState = (activeVisualization.onDrop || onDropForVisualization)?.(
+ {
+ prevState: state.visualization.state,
+ frame: framePublicAPI,
+ target,
+ source,
+ dropType,
+ group: groups.find(({ groupId: gId }) => gId === target.groupId),
+ },
+ activeVisualization
+ );
+ state.visualization.state = newVisualizationState;
- if (layerDatasourceId) {
- const {
- datasourceState: syncedDatasourceState,
- visualizationState: syncedVisualizationState,
- } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
+ if (layerDatasourceId) {
+ const {
+ datasourceState: syncedDatasourceState,
+ visualizationState: syncedVisualizationState,
+ } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
- state.datasourceStates[layerDatasourceId].state = syncedDatasourceState;
- state.visualization.state = syncedVisualizationState;
- }
- state.stagedPreview = undefined;
- },
- [setLayerDefaultDimension.type]: (
- state,
- {
- payload: { layerId, columnId, groupId },
- }: {
- payload: {
- layerId: string;
- columnId: string;
- groupId: string;
- };
- }
- ) => {
- if (!state.activeDatasourceId || !state.visualization.activeId) {
- return state;
- }
+ state.datasourceStates[layerDatasourceId].state = syncedDatasourceState;
+ state.visualization.state = syncedVisualizationState;
+ }
+ state.stagedPreview = undefined;
+ })
+ .addCase(setLayerDefaultDimension, (state, { payload: { layerId, columnId, groupId } }) => {
+ if (!state.activeDatasourceId || !state.visualization.activeId) {
+ return state;
+ }
- const activeDatasource = datasourceMap[state.activeDatasourceId];
- const activeVisualization = visualizationMap[state.visualization.activeId];
- const layerType =
- activeVisualization.getLayerType(layerId, state.visualization.state) || LayerTypes.DATA;
- const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({
- datasourceState: state.datasourceStates[state.activeDatasourceId].state,
- visualizationState: state.visualization.state,
- framePublicAPI: selectFramePublicAPI({ lens: current(state) }, datasourceMap),
- activeVisualization,
- activeDatasource,
- layerId,
- layerType,
- columnId,
- groupId,
- });
+ const activeDatasource = datasourceMap[state.activeDatasourceId];
+ const activeVisualization = visualizationMap[state.visualization.activeId];
+ const layerType =
+ activeVisualization.getLayerType(layerId, state.visualization.state) || LayerTypes.DATA;
+ const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({
+ datasourceState: state.datasourceStates[state.activeDatasourceId].state,
+ visualizationState: state.visualization.state,
+ framePublicAPI: selectFramePublicAPI({ lens: current(state) }, datasourceMap),
+ activeVisualization,
+ activeDatasource,
+ layerId,
+ layerType,
+ columnId,
+ groupId,
+ });
- state.visualization.state = activeVisualizationState;
- state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState;
- },
- [removeDimension.type]: (
- state,
- {
- payload: { layerId, columnId, datasourceId },
- }: {
- payload: {
- layerId: string;
- columnId: string;
- datasourceId?: string;
- };
- }
- ) => {
- if (!state.visualization.activeId) {
- return state;
- }
+ state.visualization.state = activeVisualizationState;
+ state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState;
+ })
+ .addCase(removeDimension, (state, { payload: { layerId, columnId, datasourceId } }) => {
+ if (!state.visualization.activeId) {
+ return state;
+ }
- const activeVisualization = visualizationMap[state.visualization.activeId];
+ const activeVisualization = visualizationMap[state.visualization.activeId];
- const links = activeVisualization.getLinkedDimensions?.(state.visualization.state);
+ const links = activeVisualization.getLinkedDimensions?.(state.visualization.state);
- const linkedDimensions = links
- ?.filter(({ from: { columnId: fromId } }) => columnId === fromId)
- ?.map(({ to }) => to);
+ const linkedDimensions = links
+ ?.filter(({ from: { columnId: fromId } }) => columnId === fromId)
+ ?.map(({ to }) => to);
- const datasource = datasourceId ? datasourceMap[datasourceId] : undefined;
+ const datasource = datasourceId ? datasourceMap[datasourceId] : undefined;
- const frame = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
+ const frame = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
- const remove = (dimensionProps: { layerId: string; columnId: string }) => {
- if (datasource && datasourceId) {
- let datasourceState;
+ const remove = (dimensionProps: { layerId: string; columnId: string }) => {
+ if (datasource && datasourceId) {
+ let datasourceState;
+ try {
+ datasourceState = current(state.datasourceStates[datasourceId].state);
+ } catch {
+ datasourceState = state.datasourceStates[datasourceId].state;
+ }
+ state.datasourceStates[datasourceId].state = datasource?.removeColumn({
+ layerId: dimensionProps.layerId,
+ columnId: dimensionProps.columnId,
+ prevState: datasourceState,
+ indexPatterns: frame.dataViews.indexPatterns,
+ });
+ }
+
+ let visualizationState;
try {
- datasourceState = current(state.datasourceStates[datasourceId].state);
+ visualizationState = current(state.visualization.state);
} catch {
- datasourceState = state.datasourceStates[datasourceId].state;
+ visualizationState = state.visualization.state;
}
- state.datasourceStates[datasourceId].state = datasource?.removeColumn({
+ state.visualization.state = activeVisualization.removeDimension({
layerId: dimensionProps.layerId,
columnId: dimensionProps.columnId,
- prevState: datasourceState,
- indexPatterns: frame.dataViews.indexPatterns,
+ prevState: visualizationState,
+ frame,
});
- }
-
- let visualizationState;
- try {
- visualizationState = current(state.visualization.state);
- } catch {
- visualizationState = state.visualization.state;
- }
- state.visualization.state = activeVisualization.removeDimension({
- layerId: dimensionProps.layerId,
- columnId: dimensionProps.columnId,
- prevState: visualizationState,
- frame,
- });
- };
+ };
- remove({ layerId, columnId });
+ remove({ layerId, columnId });
- linkedDimensions?.forEach(
- (linkedDimension) =>
- linkedDimension.columnId && // if there's no columnId, there's no dimension to remove
- remove({ columnId: linkedDimension.columnId, layerId: linkedDimension.layerId })
- );
- },
- [registerLibraryAnnotationGroup.type]: (
- state,
- {
- payload: { group, id },
- }: {
- payload: { group: EventAnnotationGroupConfig; id: string };
- }
- ) => {
- state.annotationGroups[id] = group;
- },
+ linkedDimensions?.forEach(
+ (linkedDimension) =>
+ linkedDimension.columnId && // if there's no columnId, there's no dimension to remove
+ remove({ columnId: linkedDimension.columnId, layerId: linkedDimension.layerId })
+ );
+ })
+ .addCase(registerLibraryAnnotationGroup, (state, { payload: { group, id } }) => {
+ state.annotationGroups[id] = group;
+ })
+ .addDefaultCase((state) => state);
});
};
From baa80de3d8ec4996d25e57096401e11899e2e428 Mon Sep 17 00:00:00 2001
From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
Date: Thu, 8 Feb 2024 12:56:43 +0100
Subject: [PATCH 012/104] [Lens] Fixes dimension button on configuration panel
palette is not cleaned up on changing to unsupported operation type (#175912)
Fixes https://github.com/elastic/kibana/issues/174747
This is one of the type of bugs we get quite often and I think we
reached a moment we should holistically rethink how we could validate
the state of the visualization when we update datasource state. I'll be
taking a look at that, but here's some adhoc fix!
---
.../datatable/visualization.test.tsx | 71 ++++++++++++++++++-
.../datatable/visualization.tsx | 9 ++-
2 files changed, 77 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx
index dda07ef2c41c8..963e471e912f7 100644
--- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx
+++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx
@@ -7,7 +7,13 @@
import { Ast } from '@kbn/interpreter';
import { buildExpression } from '@kbn/expressions-plugin/public';
-import { createMockDatasource, createMockFramePublicAPI, DatasourceMock } from '../../mocks';
+import {
+ createMockDatasource,
+ createMockFramePublicAPI,
+ DatasourceMock,
+ generateActiveData,
+} from '../../mocks';
+import faker from 'faker';
import { DatatableVisualizationState, getDatatableVisualization } from './visualization';
import {
Operation,
@@ -15,6 +21,7 @@ import {
FramePublicAPI,
TableSuggestionColumn,
VisualizationDimensionGroupConfig,
+ VisualizationConfigProps,
} from '../../types';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
@@ -408,6 +415,68 @@ describe('Datatable Visualization', () => {
).toEqual([{ columnId: 'c' }, { columnId: 'b' }]);
});
+ describe('with palette', () => {
+ let params: VisualizationConfigProps;
+ beforeEach(() => {
+ const datasource = createMockDatasource('test');
+ datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'b', fields: [] }]);
+ params = {
+ layerId: 'a',
+ state: {
+ layerId: 'a',
+ layerType: LayerTypes.DATA,
+ columns: [
+ {
+ columnId: 'b',
+ palette: {
+ type: 'palette' as const,
+ name: '',
+ params: { stops: [{ color: 'blue', stop: 0 }] },
+ },
+ },
+ ],
+ },
+ frame: {
+ ...mockFrame(),
+ activeData: generateActiveData([
+ {
+ id: 'a',
+ rows: Array(3).fill({
+ b: faker.random.number(),
+ }),
+ },
+ ]),
+ datasourceLayers: { a: datasource.publicAPIMock },
+ },
+ };
+ });
+
+ it('does include palette for accessor config if the values are numeric and palette exists', () => {
+ expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
+ { columnId: 'b', palette: ['blue'], triggerIconType: 'colorBy' },
+ ]);
+ });
+ it('does not include palette for accessor config if the values are not numeric and palette exists', () => {
+ params.frame.activeData = generateActiveData([
+ {
+ id: 'a',
+ rows: Array(3).fill({
+ b: faker.random.word(),
+ }),
+ },
+ ]);
+ expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
+ { columnId: 'b' },
+ ]);
+ });
+ it('does not include palette for accessor config if the values are numeric but palette exists', () => {
+ params.state.columns[0].palette = undefined;
+ expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
+ { columnId: 'b' },
+ ]);
+ });
+ });
+
it('should compute the groups correctly for text based languages', () => {
const datasource = createMockDatasource('textBased', {
isTextBasedLanguage: jest.fn(() => true),
diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx
index fe3e331d03714..05e05279567e5 100644
--- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx
@@ -14,6 +14,7 @@ import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
import { IconChartDatatable } from '@kbn/chart-icons';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common';
+import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
import type { FormBasedPersistedState } from '../../datasources/form_based/types';
import type {
SuggestionRequest,
@@ -315,16 +316,20 @@ export const getDatatableVisualization = ({
.map((accessor) => {
const columnConfig = columnMap[accessor];
const stops = columnConfig?.palette?.params?.stops;
+ const isNumeric = Boolean(
+ accessor && isNumericFieldForDatatable(frame.activeData?.[state.layerId], accessor)
+ );
const hasColoring = Boolean(columnConfig?.colorMode !== 'none' && stops);
return {
columnId: accessor,
triggerIconType: columnConfig?.hidden
? 'invisible'
- : hasColoring
+ : hasColoring && isNumeric
? 'colorBy'
: undefined,
- palette: hasColoring && stops ? stops.map(({ color }) => color) : undefined,
+ palette:
+ hasColoring && isNumeric && stops ? stops.map(({ color }) => color) : undefined,
};
}),
supportsMoreColumns: true,
From 59b986fbaa84723ceb48896c1c4ff8dc6a79ba1d Mon Sep 17 00:00:00 2001
From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
Date: Thu, 8 Feb 2024 12:21:30 +0000
Subject: [PATCH 013/104] [Security Solution][Detection Engine] sets Indicator
match rule sort order of search to asc (#176321)
## Summary
Sets search of documents for IM rule type from `desc` to `asc` when
suppression is enabled.
Also would allow to fix corner cases around [alert
suppression](https://github.com/elastic/kibana/pull/174241).
Alert suppression in IM rule relies on correct suppression time
boundaries to correctly deduplicate earlier suppressed alerts. I.e, if
document start suppression time(document timestamp) falls within
suppression boundaries, it means, alert was already suppressed. So, we
can exclude it from suppression as already suppressed and not to count
it twice.
But because documents for IM rule are searched in reverse order, it is
possible, while processing a second page of results, to falsely count
alert as already suppressed and discard it from suppressed count. That's
because its timestamp is older than document's timestamp from the first
page.
Newly added test failed only for code execution path, when number of
events is greater than number of threats.
It is because, events are split in chunks by 9,000 first. So if reverse
order in that case would cause alert from next batches to be dropped as
already suppressed
Setting `asc` can potentially affect IM rule performance, when events
need to be searched first and rule is configured with the large
look-back time. That's why new order is set to tech preview alert
suppression feature only
---------
Co-authored-by: Ryland Herrick
---
.../threat_mapping/create_event_signal.ts | 20 +--
.../threat_mapping/create_threat_signal.ts | 20 +--
.../threat_mapping/create_threat_signals.ts | 25 +++-
.../threat_mapping/get_event_count.ts | 3 +-
.../indicator_match/threat_mapping/types.ts | 7 +-
.../threat_match_alert_suppression.ts | 128 ++++++++++++++++++
6 files changed, 166 insertions(+), 37 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts
index 211466e17c8f7..acf506b0304c9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import { firstValueFrom } from 'rxjs';
-
import { buildThreatMappingFilter } from './build_threat_mapping_filter';
import { getFilter } from '../../utils/get_filter';
import { searchAfterAndBulkCreate } from '../../utils/search_after_bulk_create';
@@ -56,7 +54,8 @@ export const createEventSignal = async ({
inputIndexFields,
threatIndexFields,
completeRule,
- licensing,
+ sortOrder = 'desc',
+ isAlertSuppressionActive,
}: CreateEventSignalOptions): Promise => {
const threatFiltersFromEvents = buildThreatMappingFilter({
threatMapping,
@@ -65,9 +64,6 @@ export const createEventSignal = async ({
allowedFieldsForTermsQuery,
});
- const license = await firstValueFrom(licensing.license$);
- const hasPlatinumLicense = license.hasAtLeast('platinum');
-
if (!threatFiltersFromEvents.query || threatFiltersFromEvents.query?.bool.should.length === 0) {
// empty event list and we do not want to return everything as being
// a hit so opt to return the existing result.
@@ -134,10 +130,6 @@ export const createEventSignal = async ({
threatSearchParams,
});
- const isAlertSuppressionEnabled = Boolean(
- completeRule.ruleParams.alertSuppression?.groupBy?.length
- );
-
let createResult: SearchAfterAndBulkCreateReturnType;
const searchAfterBulkCreateParams = {
buildReasonMessage: buildReasonMessageForThreatMatchAlert,
@@ -151,7 +143,7 @@ export const createEventSignal = async ({
pageSize: searchAfterSize,
ruleExecutionLogger,
services,
- sortOrder: 'desc' as const,
+ sortOrder,
trackTotalHits: false,
tuple,
wrapHits,
@@ -160,11 +152,7 @@ export const createEventSignal = async ({
secondaryTimestamp,
};
- if (
- isAlertSuppressionEnabled &&
- runOpts.experimentalFeatures?.alertSuppressionForIndicatorMatchRuleEnabled &&
- hasPlatinumLicense
- ) {
+ if (isAlertSuppressionActive) {
createResult = await searchAfterAndBulkCreateSuppressedAlerts({
...searchAfterBulkCreateParams,
wrapSuppressedHits,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts
index c5071605841f5..7740bed7777bb 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import { firstValueFrom } from 'rxjs';
-
import { buildThreatMappingFilter } from './build_threat_mapping_filter';
import { getFilter } from '../../utils/get_filter';
import { searchAfterAndBulkCreate } from '../../utils/search_after_bulk_create';
@@ -54,7 +52,8 @@ export const createThreatSignal = async ({
allowedFieldsForTermsQuery,
inputIndexFields,
threatIndexFields,
- licensing,
+ sortOrder = 'desc',
+ isAlertSuppressionActive,
}: CreateThreatSignalOptions): Promise => {
const threatFilter = buildThreatMappingFilter({
threatMapping,
@@ -63,9 +62,6 @@ export const createThreatSignal = async ({
allowedFieldsForTermsQuery,
});
- const license = await firstValueFrom(licensing.license$);
- const hasPlatinumLicense = license.hasAtLeast('platinum');
-
if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) {
// empty threat list and we do not want to return everything as being
// a hit so opt to return the existing result.
@@ -107,10 +103,6 @@ export const createThreatSignal = async ({
threatIndexFields,
});
- const isAlertSuppressionEnabled = Boolean(
- completeRule.ruleParams.alertSuppression?.groupBy?.length
- );
-
let result: SearchAfterAndBulkCreateReturnType;
const searchAfterBulkCreateParams = {
buildReasonMessage: buildReasonMessageForThreatMatchAlert,
@@ -124,7 +116,7 @@ export const createThreatSignal = async ({
pageSize: searchAfterSize,
ruleExecutionLogger,
services,
- sortOrder: 'desc' as const,
+ sortOrder,
trackTotalHits: false,
tuple,
wrapHits,
@@ -133,11 +125,7 @@ export const createThreatSignal = async ({
secondaryTimestamp,
};
- if (
- isAlertSuppressionEnabled &&
- runOpts.experimentalFeatures?.alertSuppressionForIndicatorMatchRuleEnabled &&
- hasPlatinumLicense
- ) {
+ if (isAlertSuppressionActive) {
result = await searchAfterAndBulkCreateSuppressedAlerts({
...searchAfterBulkCreateParams,
wrapSuppressedHits,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts
index 5cb9dddc9b42e..aa108e5ea9bea 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { firstValueFrom } from 'rxjs';
+
import type { OpenPointInTimeResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { uniq, chunk } from 'lodash/fp';
@@ -216,6 +218,22 @@ export const createThreatSignals = async ({
}
};
+ const license = await firstValueFrom(licensing.license$);
+ const hasPlatinumLicense = license.hasAtLeast('platinum');
+ const isAlertSuppressionConfigured = Boolean(
+ completeRule.ruleParams.alertSuppression?.groupBy?.length
+ );
+
+ const isAlertSuppressionActive =
+ isAlertSuppressionConfigured &&
+ Boolean(runOpts.experimentalFeatures?.alertSuppressionForIndicatorMatchRuleEnabled) &&
+ hasPlatinumLicense;
+
+ // alert suppression needs to be performed on results searched in ascending order, so alert's suppression boundaries would be set correctly
+ // at the same time, there are concerns on performance of IM rule when sorting is set to asc, as it may lead to longer rule runs, since it will
+ // first go through alerts that might ve been processed in earlier executions, when look back interval set to large values (it can't be larger than 24h)
+ const sortOrder = isAlertSuppressionConfigured ? 'asc' : 'desc';
+
if (eventCount < threatListCount) {
await createSignals({
totalDocumentCount: eventCount,
@@ -236,6 +254,7 @@ export const createThreatSignals = async ({
exceptionFilter,
eventListConfig,
indexFields: inputIndexFields,
+ sortOrder,
}),
createSignal: (slicedChunk) =>
@@ -278,7 +297,8 @@ export const createThreatSignals = async ({
inputIndexFields,
threatIndexFields,
runOpts,
- licensing,
+ sortOrder,
+ isAlertSuppressionActive,
}),
});
} else {
@@ -342,7 +362,8 @@ export const createThreatSignals = async ({
inputIndexFields,
threatIndexFields,
runOpts,
- licensing,
+ sortOrder,
+ isAlertSuppressionActive,
}),
});
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts
index 214c7d3f13075..c74424f65d514 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts
@@ -29,6 +29,7 @@ export const getEventList = async ({
exceptionFilter,
eventListConfig,
indexFields,
+ sortOrder = 'desc',
}: EventsOptions): Promise> => {
const calculatedPerPage = perPage ?? MAX_PER_PAGE;
if (calculatedPerPage > 10000) {
@@ -59,7 +60,7 @@ export const getEventList = async ({
filter: queryFilter,
primaryTimestamp,
secondaryTimestamp,
- sortOrder: 'desc',
+ sortOrder,
trackTotalHits: false,
runtimeMappings,
overrideBody: eventListConfig,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts
index ae9548090ea64..d8a6a97bbc644 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts
@@ -120,7 +120,8 @@ export interface CreateThreatSignalOptions {
inputIndexFields: DataViewFieldBase[];
threatIndexFields: DataViewFieldBase[];
runOpts: RunOpts;
- licensing: LicensingPluginSetup;
+ sortOrder?: SortOrderOrUndefined;
+ isAlertSuppressionActive: boolean;
}
export interface CreateEventSignalOptions {
@@ -163,7 +164,8 @@ export interface CreateEventSignalOptions {
inputIndexFields: DataViewFieldBase[];
threatIndexFields: DataViewFieldBase[];
runOpts: RunOpts;
- licensing: LicensingPluginSetup;
+ sortOrder?: SortOrderOrUndefined;
+ isAlertSuppressionActive: boolean;
}
type EntryKey = 'field' | 'value';
@@ -312,6 +314,7 @@ export interface EventsOptions {
exceptionFilter: Filter | undefined;
eventListConfig?: OverrideBodyQuery;
indexFields: DataViewFieldBase[];
+ sortOrder?: SortOrderOrUndefined;
}
export interface EventDoc {
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts
index 9d66f49c8558b..3f53e6d8a7100 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts
@@ -1126,6 +1126,133 @@ export default ({ getService }: FtrProviderContext) => {
});
});
+ // large number of documents gets processed in batches of 9,000
+ // rule should correctly go through them and suppress
+ // that can be an issue when search results returning in desc order
+ // this test is added to verify suppression works fine for this cases
+ it('should suppress alerts on large number of documents, more than 9,000', async () => {
+ const id = uuidv4();
+ const firstTimestamp = '2020-10-28T05:45:00.000Z';
+ const secondTimestamp = '2020-10-28T06:10:00.000Z';
+
+ await eventsFiller({
+ id,
+ count: 10000 * eventsCount,
+ timestamp: [firstTimestamp, secondTimestamp],
+ });
+ await threatsFiller({ id, count: 10000 * threatsCount, timestamp: firstTimestamp });
+
+ await indexGeneratedSourceDocuments({
+ docsCount: 60000,
+ interval: [firstTimestamp, '2020-10-28T05:35:50.000Z'],
+ seed: (index, _, timestamp) => ({
+ id,
+ '@timestamp': timestamp,
+ host: {
+ name: `host-${index}`,
+ },
+ agent: { name: 'agent-a' },
+ }),
+ });
+
+ await indexGeneratedSourceDocuments({
+ docsCount: 60000,
+ interval: [secondTimestamp, '2020-10-28T06:20:50.000Z'],
+ seed: (index, _, timestamp) => ({
+ id,
+ '@timestamp': timestamp,
+ host: {
+ name: `host-${index}`,
+ },
+ agent: { name: 'agent-a' },
+ }),
+ });
+
+ await addThreatDocuments({
+ id,
+ timestamp: firstTimestamp,
+ fields: {
+ host: {
+ name: 'host-80',
+ },
+ },
+ count: 1,
+ });
+
+ await addThreatDocuments({
+ id,
+ timestamp: firstTimestamp,
+ fields: {
+ host: {
+ name: 'host-14000',
+ },
+ },
+ count: 1,
+ });
+
+ await addThreatDocuments({
+ id,
+ timestamp: firstTimestamp,
+ fields: {
+ host: {
+ name: 'host-36000',
+ },
+ },
+ count: 1,
+ });
+
+ await addThreatDocuments({
+ id,
+ timestamp: firstTimestamp,
+ fields: {
+ host: {
+ name: 'host-5700',
+ },
+ },
+ count: 1,
+ });
+
+ const rule: ThreatMatchRuleCreateProps = {
+ ...indicatorMatchRule(id),
+ alert_suppression: {
+ group_by: ['agent.name'],
+ missing_fields_strategy: 'suppress',
+ duration: {
+ value: 300,
+ unit: 'm',
+ },
+ },
+ from: 'now-35m',
+ interval: '30m',
+ };
+
+ const { previewId } = await previewRule({
+ supertest,
+ rule,
+ timeframeEnd: new Date('2020-10-28T06:30:00.000Z'),
+ invocationCount: 2,
+ });
+
+ const previewAlerts = await getPreviewAlerts({
+ es,
+ previewId,
+ sort: ['agent.name', ALERT_ORIGINAL_TIME],
+ });
+ expect(previewAlerts.length).toEqual(1);
+ expect(previewAlerts[0]._source).toEqual({
+ ...previewAlerts[0]._source,
+ [ALERT_SUPPRESSION_TERMS]: [
+ {
+ field: 'agent.name',
+ value: ['agent-a'],
+ },
+ ],
+ // There 4 documents in threats index, each matches one document in source index on each of 2 rule executions
+ // In total it gives 8 potential alerts. With suppression enabled 1 is created, the rest 7 are suppressed
+ [ALERT_SUPPRESSION_DOCS_COUNT]: 7,
+ });
+ });
+
describe('rule execution only', () => {
it('should suppress alerts during rule execution only', async () => {
const id = uuidv4();
@@ -2064,6 +2191,7 @@ export default ({ getService }: FtrProviderContext) => {
},
],
[ALERT_SUPPRESSION_DOCS_COUNT]: 499,
+ [ALERT_SUPPRESSION_START]: '2020-10-28T06:50:00.000Z',
});
});
From 479a022bd3a8ae79ca9af1eb12a90a26cb53efdf Mon Sep 17 00:00:00 2001
From: Juan Pablo Djeredjian
Date: Thu, 8 Feb 2024 13:35:54 +0100
Subject: [PATCH 014/104] [Security Solution] Improve logging for FTR test
`retry` function (#176316)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
**Fixes:**
- https://github.com/elastic/kibana/issues/175481
- https://github.com/elastic/kibana/issues/175250
### Description
Improves logging for the `retry` FTR integration testing utility that is
used to wrap helpers that make endpoint calls or direct Elasticsearch
operations.
The previous logging would only explain that the maximum amount of
retries had been reached, with the actual error caused in the test being
lost, which proved hard to debug.
These changes catches the error and log it, allowing us to understand
why a retried test failed.
Error now reported as:
```
[00:00:19] │ERROR Retrying installPrebuiltRulesPackageByVersion: Error: expected 500 "Internal Server Error", got 200 "OK"
[00:00:19] │ debg --- retry.tryForTime failed again with the same message...
[00:00:19] │ERROR Reached maximum number of retries for test: 2/2
[00:00:19] └- ✖ fail: Rules Management - Prebuilt Rules - Update Prebuilt Rules Package @ess @serverless @skipInQA update_prebuilt_rules_package should allow user to install prebuilt rules from scratch, then install new rules and upgrade existing rules from the new package
[00:00:19] │ Error: "Reached maximum number of retries for test: 2/2"
[00:00:19] │ at block (retry.ts:72:16)
[00:00:19] │ at runAttempt (retry_for_success.ts:29:21)
[00:00:19] │ at retryForSuccess (retry_for_success.ts:79:27)
[00:00:19] │ at RetryService.tryForTime (retry.ts:23:12)
[00:00:19] │ at retry (retry.ts:62:20)
[00:00:19] │ at installPrebuiltRulesPackageByVersion (install_fleet_package_by_url.ts:77:25)
[00:00:19] │ at Context. (update_prebuilt_rules_package.ts:106:46)
[00:00:19] │ at Object.apply (wrap_function.js:73:16)
```
Main error is still `"Reached maximum number of retries for test: 2/2"`,
but now additional logging of exactly **what failed in the test** is
error-logged as seen above: `ERROR Retrying
installPrebuiltRulesPackageByVersion: Error: expected 500 "Internal
Server Error", got 200 "OK"`
**Flaky test run:**
- Shared 50 and 50:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5068
- Ess 100 runs:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5091
- Serverless 100 runs:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5092
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../install_latest_bundled_prebuilt_rules.ts | 3 ++-
.../prerelease_packages.ts | 3 ++-
.../fleet_integration.ts | 1 +
.../update_prebuilt_rules_package.ts | 6 ++++--
.../detections_response/utils/retry.ts | 21 ++++++++++++++++---
.../install_fleet_package_by_url.ts | 11 ++++++++--
.../install_prebuilt_rules_fleet_package.ts | 7 +++++++
7 files changed, 43 insertions(+), 9 deletions(-)
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts
index 32eb1d3dbdf01..dcb2561b1f3e8 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts
@@ -61,7 +61,8 @@ export default ({ getService }: FtrProviderContext): void => {
es,
supertest,
'99.0.0',
- retry
+ retry,
+ log
);
// As opposed to "registry"
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts
index dcafdf8eaf1a7..3ed663f7ecc66 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts
@@ -49,7 +49,8 @@ export default ({ getService }: FtrProviderContext): void => {
const fleetPackageInstallationResponse = await installPrebuiltRulesPackageViaFleetAPI(
es,
supertest,
- retry
+ retry,
+ log
);
expect(fleetPackageInstallationResponse.items.length).toBe(1);
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts
index 1233af5c33f6a..1a8394a3b5144 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts
@@ -49,6 +49,7 @@ export default ({ getService }: FtrProviderContext): void => {
supertest,
overrideExistingPackage: true,
retryService: retry,
+ log,
});
// Verify that status is updated after package installation
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts
index 11577bec1b5c7..ffba2bd01d988 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts
@@ -107,7 +107,8 @@ export default ({ getService }: FtrProviderContext): void => {
es,
supertest,
previousVersion,
- retry
+ retry,
+ log
);
expect(installPreviousPackageResponse._meta.install_source).toBe('registry');
@@ -160,7 +161,8 @@ export default ({ getService }: FtrProviderContext): void => {
es,
supertest,
currentVersion,
- retry
+ retry,
+ log
);
expect(installLatestPackageResponse.items.length).toBeGreaterThanOrEqual(0);
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts
index dafd16aaa9f5f..3007448ed895f 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts
@@ -6,7 +6,7 @@
*/
import { RetryService } from '@kbn/ftr-common-functional-services';
-
+import type { ToolingLog } from '@kbn/tooling-log';
/**
* Retry wrapper for async supertests, with a maximum number of retries.
* You can pass in a function that executes a supertest test, and make assertions
@@ -44,15 +44,19 @@ import { RetryService } from '@kbn/ftr-common-functional-services';
export const retry = async ({
test,
retryService,
+ utilityName,
retries = 2,
timeout = 30000,
retryDelay = 200,
+ log,
}: {
test: () => Promise;
+ utilityName: string;
retryService: RetryService;
retries?: number;
timeout?: number;
retryDelay?: number;
+ log: ToolingLog;
}): Promise => {
let retryAttempt = 0;
const response = await retryService.tryForTime(
@@ -61,12 +65,23 @@ export const retry = async ({
if (retryAttempt > retries) {
// Log error message if we reached the maximum number of retries
// but don't throw an error, return it to break the retry loop.
- return new Error('Reached maximum number of retries for test.');
+ const errorMessage = `Reached maximum number of retries for test: ${
+ retryAttempt - 1
+ }/${retries}`;
+ log?.error(errorMessage);
+ return new Error(JSON.stringify(errorMessage));
}
retryAttempt = retryAttempt + 1;
- return test();
+ // Catch the error thrown by the test and log it, then throw it again
+ // to cause `tryForTime` to retry.
+ try {
+ return await test();
+ } catch (error) {
+ log.error(`Retrying ${utilityName}: ${error}`);
+ throw error;
+ }
},
undefined,
retryDelay
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts
index 988d73660d0ee..2839795ab1976 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts
@@ -9,6 +9,7 @@ import type SuperTest from 'supertest';
import { InstallPackageResponse } from '@kbn/fleet-plugin/common/types';
import { epmRouteService } from '@kbn/fleet-plugin/common';
import { RetryService } from '@kbn/ftr-common-functional-services';
+import type { ToolingLog } from '@kbn/tooling-log';
import expect from 'expect';
import { retry } from '../../retry';
import { refreshSavedObjectIndices } from '../../refresh_index';
@@ -28,7 +29,8 @@ const ATTEMPT_TIMEOUT = 120000;
export const installPrebuiltRulesPackageViaFleetAPI = async (
es: Client,
supertest: SuperTest.SuperTest,
- retryService: RetryService
+ retryService: RetryService,
+ log: ToolingLog
): Promise => {
const fleetResponse = await retry({
test: async () => {
@@ -44,9 +46,11 @@ export const installPrebuiltRulesPackageViaFleetAPI = async (
return testResponse.body;
},
+ utilityName: installPrebuiltRulesPackageViaFleetAPI.name,
retryService,
retries: MAX_RETRIES,
timeout: ATTEMPT_TIMEOUT,
+ log,
});
await refreshSavedObjectIndices(es);
@@ -67,7 +71,8 @@ export const installPrebuiltRulesPackageByVersion = async (
es: Client,
supertest: SuperTest.SuperTest,
version: string,
- retryService: RetryService
+ retryService: RetryService,
+ log: ToolingLog
): Promise => {
const fleetResponse = await retry({
test: async () => {
@@ -83,9 +88,11 @@ export const installPrebuiltRulesPackageByVersion = async (
return testResponse.body;
},
+ utilityName: installPrebuiltRulesPackageByVersion.name,
retryService,
retries: MAX_RETRIES,
timeout: ATTEMPT_TIMEOUT,
+ log,
});
await refreshSavedObjectIndices(es);
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts
index 592406e8c3398..770d966f50a59 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts
@@ -15,6 +15,7 @@ import { InstallPackageResponse } from '@kbn/fleet-plugin/common/types';
import type SuperTest from 'supertest';
import { RetryService } from '@kbn/ftr-common-functional-services';
import expect from 'expect';
+import { ToolingLog } from '@kbn/tooling-log';
import { retry } from '../../retry';
import { refreshSavedObjectIndices } from '../../refresh_index';
@@ -35,12 +36,14 @@ export const installPrebuiltRulesFleetPackage = async ({
version,
overrideExistingPackage,
retryService,
+ log,
}: {
es: Client;
supertest: SuperTest.SuperTest;
version?: string;
overrideExistingPackage: boolean;
retryService: RetryService;
+ log: ToolingLog;
}): Promise => {
if (version) {
// Install a specific version
@@ -59,8 +62,10 @@ export const installPrebuiltRulesFleetPackage = async ({
return testResponse.body;
},
retryService,
+ utilityName: installPrebuiltRulesFleetPackage.name,
retries: MAX_RETRIES,
timeout: ATTEMPT_TIMEOUT,
+ log,
});
await refreshSavedObjectIndices(es);
@@ -91,8 +96,10 @@ export const installPrebuiltRulesFleetPackage = async ({
return body;
},
retryService,
+ utilityName: installPrebuiltRulesFleetPackage.name,
retries: MAX_RETRIES,
timeout: ATTEMPT_TIMEOUT,
+ log,
});
await refreshSavedObjectIndices(es);
From 1c3fa24be396176454df3dd3a67c7acdfcea46d4 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Thu, 8 Feb 2024 13:53:29 +0100
Subject: [PATCH 015/104] Add `build_flavor` to `/api/status` response
(#176477)
## Summary
Fix https://github.com/elastic/kibana/issues/176475
Add the `version.build_flavor` field to the response of the status API
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../src/status/components/version_header.test.tsx | 1 +
.../src/status/lib/load_status.test.ts | 1 +
packages/core/status/core-status-common-internal/src/status.ts | 2 ++
packages/core/status/core-status-common-internal/tsconfig.json | 3 ++-
.../status/core-status-server-internal/src/routes/status.ts | 3 ++-
src/core/server/integration_tests/status/routes/status.test.ts | 1 +
6 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx
index 8172b705a6ffe..e0eaf9b2144fa 100644
--- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx
+++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx
@@ -17,6 +17,7 @@ const buildServerVersion = (parts: Partial = {}): ServerVersion =
build_number: 9000,
build_snapshot: false,
build_date: '2023-05-15T23:12:09.000Z',
+ build_flavor: 'traditional',
...parts,
});
diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts
index ed6cc186313f3..92aad0f460e70 100644
--- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts
+++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts
@@ -21,6 +21,7 @@ const mockedResponse: StatusResponse = {
build_number: 12,
build_snapshot: false,
build_date: '2023-05-15T23:12:09.000Z',
+ build_flavor: 'traditional',
},
status: {
overall: {
diff --git a/packages/core/status/core-status-common-internal/src/status.ts b/packages/core/status/core-status-common-internal/src/status.ts
index 806a3ac56b407..0a3f3ad770fe8 100644
--- a/packages/core/status/core-status-common-internal/src/status.ts
+++ b/packages/core/status/core-status-common-internal/src/status.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import type { BuildFlavor } from '@kbn/config';
import type { ServiceStatusLevelId, ServiceStatus, CoreStatus } from '@kbn/core-status-common';
import type { OpsMetrics } from '@kbn/core-metrics-server';
@@ -34,6 +35,7 @@ export interface ServerVersion {
build_hash: string;
build_number: number;
build_snapshot: boolean;
+ build_flavor: BuildFlavor;
build_date: string;
}
diff --git a/packages/core/status/core-status-common-internal/tsconfig.json b/packages/core/status/core-status-common-internal/tsconfig.json
index c746e7133cd2c..7d31fa090eb0f 100644
--- a/packages/core/status/core-status-common-internal/tsconfig.json
+++ b/packages/core/status/core-status-common-internal/tsconfig.json
@@ -13,7 +13,8 @@
],
"kbn_references": [
"@kbn/core-status-common",
- "@kbn/core-metrics-server"
+ "@kbn/core-metrics-server",
+ "@kbn/config"
],
"exclude": [
"target/**/*",
diff --git a/packages/core/status/core-status-server-internal/src/routes/status.ts b/packages/core/status/core-status-server-internal/src/routes/status.ts
index 59c7aa23c51d4..1faa623467b58 100644
--- a/packages/core/status/core-status-server-internal/src/routes/status.ts
+++ b/packages/core/status/core-status-server-internal/src/routes/status.ts
@@ -156,7 +156,7 @@ const getFullStatusResponse = async ({
};
query: { v8format?: boolean; v7format?: boolean };
}): Promise => {
- const { version, buildSha, buildNum, buildDate } = config.packageInfo;
+ const { version, buildSha, buildNum, buildDate, buildFlavor } = config.packageInfo;
const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, '');
let statusInfo: StatusInfo | LegacyStatusInfo;
@@ -186,6 +186,7 @@ const getFullStatusResponse = async ({
build_hash: buildSha,
build_number: buildNum,
build_snapshot: SNAPSHOT_POSTFIX.test(version),
+ build_flavor: buildFlavor,
build_date: buildDate.toISOString(),
},
status: statusInfo,
diff --git a/src/core/server/integration_tests/status/routes/status.test.ts b/src/core/server/integration_tests/status/routes/status.test.ts
index 755b9fc9b46f6..29a55743dd0d5 100644
--- a/src/core/server/integration_tests/status/routes/status.test.ts
+++ b/src/core/server/integration_tests/status/routes/status.test.ts
@@ -210,6 +210,7 @@ describe('GET /api/status', () => {
build_number: 1234,
build_snapshot: true,
build_date: new Date('2023-05-15T23:12:09.000Z').toISOString(),
+ build_flavor: 'traditional',
});
const metricsMockValue = await firstValueFrom(metrics.getOpsMetrics$());
expect(result.body.metrics).toEqual({
From 0ed5fe66476cbb8d891ec3ba1494251316334ac6 Mon Sep 17 00:00:00 2001
From: Marco Vettorello
Date: Thu, 8 Feb 2024 14:24:10 +0100
Subject: [PATCH 016/104] [Lens] Color mapping UX refactoring (#175144)
This commit revisits the UX for the Lens color mapping applying a
slightly different UX behaviour to allow looping of colors from a chosen
palette.
---
.../__stories__/color_mapping.stories.tsx | 165 ++++-----
.../categorical_color_mapping.test.tsx | 20 +-
.../color/color_handling.test.ts | 179 +++++++--
.../color_mapping/color/color_handling.ts | 137 ++++---
.../color_mapping/color/rule_matching.ts | 2 +-
.../components/assignment/assignment.tsx | 14 +-
.../components/assignment/match.tsx | 7 +-
.../components/assignment/range.tsx | 5 +-
.../assignment/special_assignment.tsx | 69 ++--
.../components/color_picker/color_picker.tsx | 2 +-
.../components/color_picker/color_swatch.tsx | 14 +-
.../color_picker/palette_colors.tsx | 8 +-
.../components/color_picker/rgb_picker.tsx | 5 +-
.../components/container/assigments.tsx | 329 +++++++++++++++++
.../components/container/container.tsx | 282 +++++---------
.../container/unassigned_terms_config.tsx | 150 ++++++++
.../components/palette_selector/gradient.tsx | 345 +++++-------------
.../palette_selector/gradient_add_stop.tsx | 110 ++++++
.../palette_selector/palette_selector.tsx | 206 ++---------
.../components/palette_selector/scale.tsx | 144 ++++++++
.../config/assignment_from_categories.ts | 65 ----
.../color_mapping/config/assignments.ts | 74 +---
.../config/default_color_mapping.ts | 55 +--
.../color_mapping/config/types.ts | 12 +-
.../shared_components/color_mapping/index.ts | 1 +
.../color_mapping/palettes/elastic_brand.ts | 6 +-
.../color_mapping/palettes/eui_amsterdam.ts | 6 +-
.../color_mapping/palettes/kibana_legacy.ts | 6 +-
.../color_mapping/state/color_mapping.ts | 25 +-
.../color_mapping/state/selectors.ts | 6 +-
.../color_telemetry_helpers.test.ts | 60 +--
.../color_telemetry_helpers.ts | 33 +-
.../translations/translations/fr-FR.json | 7 +-
.../translations/translations/ja-JP.json | 7 +-
.../translations/translations/zh-CN.json | 7 +-
.../test/functional/page_objects/lens_page.ts | 5 +-
36 files changed, 1434 insertions(+), 1134 deletions(-)
create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx
create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx
create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx
create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx
delete mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx
index 95f4ff5623ea3..f1d9add2c0f09 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx
@@ -6,122 +6,115 @@
* Side Public License, v 1.
*/
-import React, { FC } from 'react';
-import { EuiFlyout, EuiForm } from '@elastic/eui';
+import React, { FC, useState } from 'react';
+import { EuiFlyout, EuiForm, EuiPage, isColorDark } from '@elastic/eui';
import { ComponentStory } from '@storybook/react';
+import { css } from '@emotion/react';
import { CategoricalColorMapping, ColorMappingProps } from '../categorical_color_mapping';
-import { AVAILABLE_PALETTES } from '../palettes';
+import { AVAILABLE_PALETTES, getPalette, NeutralPalette } from '../palettes';
import { DEFAULT_COLOR_MAPPING_CONFIG } from '../config/default_color_mapping';
+import { ColorMapping } from '../config';
+import { getColorFactory } from '../color/color_handling';
+import { ruleMatch } from '../color/rule_matching';
+import { getValidColor } from '../color/color_math';
export default {
title: 'Color Mapping',
component: CategoricalColorMapping,
- decorators: [
- (story: Function) => (
- {}} hideCloseButton>
- {story()}
-
- ),
- ],
+ decorators: [(story: Function) => story()],
};
-const Template: ComponentStory> = (args) => (
-
-);
+const Template: ComponentStory> = (args) => {
+ const [updatedModel, setUpdateModel] = useState(
+ DEFAULT_COLOR_MAPPING_CONFIG
+ );
+
+ const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette);
+
+ const colorFactory = getColorFactory(updatedModel, getPaletteFn, false, args.data);
+
+ return (
+
+
+ {args.data.type === 'categories' &&
+ args.data.categories.map((c, i) => {
+ const match = updatedModel.assignments.some(({ rule }) => {
+ return ruleMatch(rule, c);
+ });
+ const color = colorFactory(c);
+ const isDark = isColorDark(...getValidColor(color).rgb());
+ return (
+
+ {c}
+
+ );
+ })}
+
+ {}}
+ hideCloseButton
+ ownFocus={false}
+ >
+
+
+
+
+
+ );
+};
export const Default = Template.bind({});
Default.args = {
model: {
...DEFAULT_COLOR_MAPPING_CONFIG,
- assignmentMode: 'manual',
+
colorMode: {
- type: 'gradient',
- steps: [
- {
- type: 'categorical',
- colorIndex: 0,
- paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId,
- touched: false,
- },
- {
- type: 'categorical',
- colorIndex: 1,
- paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId,
- touched: false,
- },
- {
- type: 'categorical',
- colorIndex: 2,
- paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId,
- touched: false,
- },
- ],
- sort: 'asc',
+ type: 'categorical',
},
- assignments: [
- {
- rule: {
- type: 'matchExactly',
- values: ['this is', 'a multi-line combobox that is very long and that will be truncated'],
- },
- color: {
- type: 'gradient',
- },
- touched: false,
- },
- {
- rule: {
- type: 'matchExactly',
- values: ['b', ['double', 'value']],
- },
- color: {
- type: 'gradient',
- },
- touched: false,
- },
- {
- rule: {
- type: 'matchExactly',
- values: ['c'],
- },
- color: {
- type: 'gradient',
- },
- touched: false,
- },
+ specialAssignments: [
{
rule: {
- type: 'matchExactly',
- values: [
- 'this is',
- 'a multi-line wrap',
- 'combo box',
- 'test combo',
- '3 lines',
- ['double', 'value'],
- ],
+ type: 'other',
},
color: {
- type: 'gradient',
+ type: 'loop',
},
touched: false,
},
],
+ assignments: [],
},
isDarkMode: false,
data: {
type: 'categories',
categories: [
- 'a',
- 'b',
- 'c',
- 'd',
- 'this is',
- 'a multi-line wrap',
- 'combo box',
- 'test combo',
- '3 lines',
+ 'US',
+ 'Mexico',
+ 'Brasil',
+ 'Canada',
+ 'Italy',
+ 'Germany',
+ 'France',
+ 'Spain',
+ 'UK',
+ 'Portugal',
+ 'Greece',
+ 'Sweden',
+ 'Finland',
],
},
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx
index fe8374d7dcdcd..ccc955d2b8947 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx
@@ -13,8 +13,9 @@ import { AVAILABLE_PALETTES } from './palettes';
import { DEFAULT_COLOR_MAPPING_CONFIG } from './config/default_color_mapping';
import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common';
-const AUTO_ASSIGN_SWITCH = '[data-test-subj="lns-colorMapping-autoAssignSwitch"]';
const ASSIGNMENTS_LIST = '[data-test-subj="lns-colorMapping-assignmentsList"]';
+const ASSIGNMENTS_PROMPT = '[data-test-subj="lns-colorMapping-assignmentsPrompt"]';
+const ASSIGNMENTS_PROMPT_ADD_ALL = '[data-test-subj="lns-colorMapping-assignmentsPromptAddAll"]';
const ASSIGNMENT_ITEM = (i: number) => `[data-test-subj="lns-colorMapping-assignmentsItem${i}"]`;
describe('color mapping', () => {
@@ -35,19 +36,12 @@ describe('color mapping', () => {
/>
);
- expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(true);
- expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual(
- dataInput.categories.length
- );
- dataInput.categories.forEach((category, index) => {
- const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes();
- expect(assignment.text()).toEqual(category);
- expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(true);
- });
+ // empty list prompt visible
+ expect(component.find(ASSIGNMENTS_PROMPT)).toBeTruthy();
expect(onModelUpdateFn).not.toBeCalled();
});
- it('switch to manual assignments', () => {
+ it('Add all terms to assignments', () => {
const dataInput: ColorMappingInputData = {
type: 'categories',
categories: ['categoryA', 'categoryB'],
@@ -63,9 +57,8 @@ describe('color mapping', () => {
specialTokens={new Map()}
/>
);
- component.find(AUTO_ASSIGN_SWITCH).hostNodes().simulate('click');
+ component.find(ASSIGNMENTS_PROMPT_ADD_ALL).hostNodes().simulate('click');
expect(onModelUpdateFn).toBeCalledTimes(1);
- expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(false);
expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual(
dataInput.categories.length
);
@@ -97,6 +90,7 @@ describe('color mapping', () => {
}
/>
);
+ component.find(ASSIGNMENTS_PROMPT_ADD_ALL).hostNodes().simulate('click');
expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual(
dataInput.categories.length
);
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts
index 93896394daf41..f8631ed5768da 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts
@@ -23,6 +23,7 @@ import { ColorMapping } from '../config';
describe('Color mapping - color generation', () => {
const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette);
+
it('returns EUI light colors from default config', () => {
const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, {
type: 'categories',
@@ -31,18 +32,36 @@ describe('Color mapping - color generation', () => {
expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
- // if the category is not available in the `categories` list then a default neutral color is used
- expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ // if the category is not available in the `categories` list then a default netural is used
+ // this is an edge case and ideally never happen
+ expect(colorFactory('not_available_1')).toBe(
+ NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]
+ );
});
- it('returns max number of colors defined in palette, use other color otherwise', () => {
+ // currently there is no difference in the two colors, but this could change in the future
+ // this test will catch the change
+ it('returns EUI dark colors from default config', () => {
+ const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, true, {
+ type: 'categories',
+ categories: ['catA', 'catB', 'catC'],
+ });
+ expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
+ expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
+ expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
+ // if the category is not available in the `categories` list then a default netural is used
+ // this is an edge case and ideally never happen
+ expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ });
+
+ it('by default loops colors defined in palette', () => {
const twoColorPalette: ColorMapping.CategoricalPalette = {
id: 'twoColors',
name: 'twoColors',
colorCount: 2,
type: 'categorical',
- getColor(valueInRange, isDarkMode) {
- return ['red', 'blue'][valueInRange];
+ getColor(indexInRange, isDarkMode, loop) {
+ return ['red', 'blue'][loop ? indexInRange % 2 : indexInRange];
},
};
@@ -53,6 +72,17 @@ describe('Color mapping - color generation', () => {
const colorFactory = getColorFactory(
{
...DEFAULT_COLOR_MAPPING_CONFIG,
+ specialAssignments: [
+ {
+ color: {
+ type: 'loop',
+ },
+ rule: {
+ type: 'other',
+ },
+ touched: false,
+ },
+ ],
paletteId: twoColorPalette.id,
},
simplifiedGetPaletteGn,
@@ -64,23 +94,58 @@ describe('Color mapping - color generation', () => {
);
expect(colorFactory('cat1')).toBe('#ff0000');
expect(colorFactory('cat2')).toBe('#0000ff');
- // return a palette color only up to the max number of color in the palette
- expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
- expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ // the palette will loop depending on the number of colors available
+ expect(colorFactory('cat3')).toBe('#ff0000');
+ expect(colorFactory('cat4')).toBe('#0000ff');
});
- // currently there is no difference in the two colors, but this could change in the future
- // this test will catch the change
- it('returns EUI dark colors from default config', () => {
- const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, true, {
- type: 'categories',
- categories: ['catA', 'catB', 'catC'],
- });
- expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
- expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
- expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
- // if the category is not available in the `categories` list then a default neutral color is used
- expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ it('returns the unassigned color if configured statically', () => {
+ const twoColorPalette: ColorMapping.CategoricalPalette = {
+ id: 'twoColors',
+ name: 'twoColors',
+ colorCount: 2,
+ type: 'categorical',
+ getColor(indexInRange, darkMode, loop) {
+ return ['red', 'blue'][loop ? indexInRange % 2 : indexInRange];
+ },
+ };
+
+ const simplifiedGetPaletteGn = getPalette(
+ new Map([[twoColorPalette.id, twoColorPalette]]),
+ NeutralPalette
+ );
+ const colorFactory = getColorFactory(
+ {
+ ...DEFAULT_COLOR_MAPPING_CONFIG,
+ specialAssignments: [
+ {
+ color: {
+ type: 'categorical',
+ paletteId: NeutralPalette.id,
+ colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX,
+ },
+ rule: {
+ type: 'other',
+ },
+ touched: false,
+ },
+ ],
+ paletteId: twoColorPalette.id,
+ },
+ simplifiedGetPaletteGn,
+ false,
+ {
+ type: 'categories',
+ categories: ['cat1', 'cat2', 'cat3', 'cat4'],
+ }
+ );
+ expect(colorFactory('cat1')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ expect(colorFactory('cat2')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
+ // if the category is not available in the `categories` list then a default netural is used
+ // this is an edge case and ideally never happen
+ expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
});
it('handles special tokens, multi-field categories and non-trimmed whitespaces', () => {
@@ -89,19 +154,19 @@ describe('Color mapping - color generation', () => {
categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '],
});
expect(colorFactory('__other__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
- expect(colorFactory(['fieldA', 'fieldB'])).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
+ // expect(colorFactory(['fieldA', 'fieldB'])).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
expect(colorFactory('__empty__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
expect(colorFactory(' with-whitespaces ')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[3]);
});
- it('ignores configured assignments in auto mode', () => {
+ it('ignores configured assignments in loop mode', () => {
const colorFactory = getColorFactory(
{
...DEFAULT_COLOR_MAPPING_CONFIG,
assignments: [
{
color: { type: 'colorCode', colorCode: 'red' },
- rule: { type: 'matchExactly', values: ['assignmentToIgnore'] },
+ rule: { type: 'matchExactly', values: ['configuredAssignment'] },
touched: false,
},
],
@@ -110,19 +175,19 @@ describe('Color mapping - color generation', () => {
false,
{
type: 'categories',
- categories: ['catA', 'catB', 'assignmentToIgnore'],
+ categories: ['catA', 'catB', 'configuredAssignment', 'nextCat'],
}
);
expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
- expect(colorFactory('assignmentToIgnore')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
+ expect(colorFactory('configuredAssignment')).toBe('red');
+ expect(colorFactory('nextCat')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
});
it('color with auto rule are assigned in order of the configured data input', () => {
const colorFactory = getColorFactory(
{
...DEFAULT_COLOR_MAPPING_CONFIG,
- assignmentMode: 'manual',
assignments: [
{
color: { type: 'colorCode', colorCode: 'red' },
@@ -154,7 +219,8 @@ describe('Color mapping - color generation', () => {
expect(colorFactory('redCat')).toBe('red');
// this matches with the second availabe "auto" rule
expect(colorFactory('greenCat')).toBe('green');
- // if the category is not available in the `categories` list then a default neutral color is used
+ // if the category is not available in the `categories` list then a default netural is used
+ // this is an edge case and ideally never happen
expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
});
@@ -188,7 +254,7 @@ describe('Color mapping - color generation', () => {
expect(toHex(colorFactory('cat3'))).toBe('#cce8e0');
});
- it('returns sequential gradient colors from lighter to darker [asc, lightMode]', () => {
+ it('sequential gradient colors from lighter to darker [asc, lightMode]', () => {
const colorFactory = getColorFactory(
{
...DEFAULT_COLOR_MAPPING_CONFIG,
@@ -212,10 +278,59 @@ describe('Color mapping - color generation', () => {
categories: ['cat1', 'cat2', 'cat3'],
}
);
+ // light green
expect(toHex(colorFactory('cat1'))).toBe('#cce8e0');
+ // mid green point
expect(toHex(colorFactory('cat2'))).toBe('#93cebc');
+ // initial gradient color
+ expect(toHex(colorFactory('cat3'))).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
+ });
+ it('sequential gradients and static color from lighter to darker [asc, lightMode]', () => {
+ const colorFactory = getColorFactory(
+ {
+ ...DEFAULT_COLOR_MAPPING_CONFIG,
+ assignments: [
+ { color: { type: 'gradient' }, rule: { type: 'auto' }, touched: false },
+ { color: { type: 'gradient' }, rule: { type: 'auto' }, touched: false },
+ ],
+
+ colorMode: {
+ type: 'gradient',
+ steps: [
+ {
+ type: 'categorical',
+ paletteId: EUIAmsterdamColorBlindPalette.id,
+ colorIndex: 0,
+ touched: false,
+ },
+ ],
+ sort: 'asc',
+ },
+ specialAssignments: [
+ {
+ color: {
+ type: 'categorical',
+ colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX,
+ paletteId: NeutralPalette.id,
+ },
+ rule: {
+ type: 'other',
+ },
+ touched: false,
+ },
+ ],
+ },
+ getPaletteFn,
+ false,
+ {
+ type: 'categories',
+ categories: ['cat1', 'cat2', 'cat3'],
+ }
+ );
+ expect(toHex(colorFactory('cat1'))).toBe('#cce8e0');
+ expect(toHex(colorFactory('cat2'))).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
// this matches exactly with the initial step selected
- expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0]));
+ expect(toHex(colorFactory('cat3'))).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
});
it('returns 2 colors gradient [desc, lightMode]', () => {
@@ -287,8 +402,8 @@ describe('Color mapping - color generation', () => {
expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink
expect(toHex(colorFactory('cat2'))).toBe(NEUTRAL_COLOR_DARK[0]); // NEUTRAL LIGHT GRAY
expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green
- expect(toHex(colorFactory('not available cat'))).toBe(
- toHex(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX])
- ); // check the other
+ // if the category is not available in the `categories` list then a default netural is used
+ // this is an edge case and ideally never happen
+ expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]);
});
});
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts
index 795f94b740e9b..8867b07572308 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts
@@ -6,25 +6,32 @@
* Side Public License, v 1.
*/
import chroma from 'chroma-js';
+import { findLast } from 'lodash';
import { ColorMapping } from '../config';
import { changeAlpha, combineColors, getValidColor } from './color_math';
-import { generateAutoAssignmentsForCategories } from '../config/assignment_from_categories';
-import { getPalette } from '../palettes';
+import { getPalette, NeutralPalette } from '../palettes';
import { ColorMappingInputData } from '../categorical_color_mapping';
import { ruleMatch } from './rule_matching';
import { GradientColorMode } from '../config/types';
+import {
+ DEFAULT_NEUTRAL_PALETTE_INDEX,
+ DEFAULT_OTHER_ASSIGNMENT_INDEX,
+} from '../config/default_color_mapping';
export function getAssignmentColor(
colorMode: ColorMapping.Config['colorMode'],
- color: ColorMapping.Config['assignments'][number]['color'],
+ color:
+ | ColorMapping.Config['assignments'][number]['color']
+ | (ColorMapping.LoopColor & { paletteId: string; colorIndex: number }),
getPaletteFn: ReturnType,
isDarkMode: boolean,
index: number,
total: number
-) {
+): string {
switch (color.type) {
case 'colorCode':
case 'categorical':
+ case 'loop':
return getColor(color, getPaletteFn, isDarkMode);
case 'gradient': {
if (colorMode.type === 'categorical') {
@@ -37,31 +44,28 @@ export function getAssignmentColor(
}
export function getColor(
- color: ColorMapping.ColorCode | ColorMapping.CategoricalColor,
+ color:
+ | ColorMapping.ColorCode
+ | ColorMapping.CategoricalColor
+ | (ColorMapping.LoopColor & { paletteId: string; colorIndex: number }),
getPaletteFn: ReturnType,
isDarkMode: boolean
-) {
+): string {
return color.type === 'colorCode'
? color.colorCode
- : getValidColor(getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)).hex();
+ : getValidColor(
+ getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode, true)
+ ).hex();
}
export function getColorFactory(
- model: ColorMapping.Config,
+ { assignments, specialAssignments, colorMode, paletteId }: ColorMapping.Config,
getPaletteFn: ReturnType,
isDarkMode: boolean,
data: ColorMappingInputData
): (category: string | string[]) => string {
- const palette = getPaletteFn(model.paletteId);
- // generate on-the-fly assignments in auto-mode based on current data.
- // This simplify the code by always using assignments, even if there is no real static assigmnets
- const assignments =
- model.assignmentMode === 'auto'
- ? generateAutoAssignmentsForCategories(data, palette, model.colorMode)
- : model.assignments;
-
// find auto-assigned colors
- const autoAssignedColors =
+ const autoByOrderAssignments =
data.type === 'categories'
? assignments.filter((a) => {
return (
@@ -71,75 +75,90 @@ export function getColorFactory(
: [];
// find all categories that doesn't match with an assignment
- const nonAssignedCategories =
+ const notAssignedCategories =
data.type === 'categories'
? data.categories.filter((category) => {
return !assignments.some(({ rule }) => ruleMatch(rule, category));
})
: [];
+ const lastCategorical = findLast(assignments, (d) => {
+ return d.color.type === 'categorical';
+ });
+ const nextCategoricalIndex =
+ lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0;
+
return (category: string | string[]) => {
if (typeof category === 'string' || Array.isArray(category)) {
- const nonAssignedCategoryIndex = nonAssignedCategories.indexOf(category);
+ const nonAssignedCategoryIndex = notAssignedCategories.indexOf(category);
- // return color for a non assigned category
+ // this category is not assigned to a specific color
if (nonAssignedCategoryIndex > -1) {
- if (nonAssignedCategoryIndex < autoAssignedColors.length) {
+ // if the category order is within current number of auto-assigned items pick the defined color
+ if (nonAssignedCategoryIndex < autoByOrderAssignments.length) {
const autoAssignmentIndex = assignments.findIndex(
- (d) => d === autoAssignedColors[nonAssignedCategoryIndex]
+ (d) => d === autoByOrderAssignments[nonAssignedCategoryIndex]
);
return getAssignmentColor(
- model.colorMode,
- autoAssignedColors[nonAssignedCategoryIndex].color,
+ colorMode,
+ autoByOrderAssignments[nonAssignedCategoryIndex].color,
getPaletteFn,
isDarkMode,
autoAssignmentIndex,
assignments.length
);
}
- // if no auto-assign color rule/color is available then use the other color
- // TODO: the specialAssignment[0] position is arbitrary, we should fix it better
- return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode);
- }
+ const totalColorsIfGradient = assignments.length || notAssignedCategories.length;
+ const indexIfGradient =
+ (nonAssignedCategoryIndex - autoByOrderAssignments.length) % totalColorsIfGradient;
- // find the assignment where the category matches the rule
- const matchingAssignmentIndex = assignments.findIndex(({ rule }) => {
- return ruleMatch(rule, category);
- });
-
- // return the assigned color
- if (matchingAssignmentIndex > -1) {
- const assignment = assignments[matchingAssignmentIndex];
+ // if no auto-assign color rule/color is available then use the color looping palette
return getAssignmentColor(
- model.colorMode,
- assignment.color,
+ colorMode,
+ // TODO: the specialAssignment[0] position is arbitrary, we should fix it better
+ specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color.type === 'loop'
+ ? colorMode.type === 'gradient'
+ ? { type: 'gradient' }
+ : {
+ type: 'loop',
+ // those are applied here and depends on the current non-assigned category - auto-assignment list
+ colorIndex:
+ nonAssignedCategoryIndex - autoByOrderAssignments.length + nextCategoricalIndex,
+ paletteId,
+ }
+ : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color,
getPaletteFn,
isDarkMode,
- matchingAssignmentIndex,
- assignments.length
+ indexIfGradient,
+ totalColorsIfGradient
);
}
- // if no assign color rule/color is available then use the other color
- // TODO: the specialAssignment[0] position is arbitrary, we should fix it better
- return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode);
- } else {
- const matchingAssignmentIndex = assignments.findIndex(({ rule }) => {
- return ruleMatch(rule, category);
- });
+ }
+ // find the assignment where the category matches the rule
+ const matchingAssignmentIndex = assignments.findIndex(({ rule }) => {
+ return ruleMatch(rule, category);
+ });
- if (matchingAssignmentIndex > -1) {
- const assignment = assignments[matchingAssignmentIndex];
- return getAssignmentColor(
- model.colorMode,
- assignment.color,
- getPaletteFn,
- isDarkMode,
- matchingAssignmentIndex,
- assignments.length
- );
- }
- return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode);
+ if (matchingAssignmentIndex > -1) {
+ const assignment = assignments[matchingAssignmentIndex];
+ return getAssignmentColor(
+ colorMode,
+ assignment.color,
+ getPaletteFn,
+ isDarkMode,
+ matchingAssignmentIndex,
+ assignments.length
+ );
}
+ return getColor(
+ {
+ type: 'categorical',
+ paletteId: NeutralPalette.id,
+ colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX,
+ },
+ getPaletteFn,
+ isDarkMode
+ );
};
}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts
index 0d844ca26e27e..a157c7927747c 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts
@@ -23,7 +23,7 @@ export function ruleMatch(
}
return rule.values.includes(`${value}`);
case 'matchExactlyCI':
- return rule.values.some((d) => d.toLowerCase() === `${value}`);
+ return rule.values.some((d) => d.toLowerCase() === `${value}`.toLowerCase());
case 'range':
// TODO: color by value not yet possible in all charts in elastic-charts
return typeof value === 'number' ? rangeMatch(rule, value) : false;
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx
index 896f2ea392884..89c4375d4bc10 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx
@@ -31,8 +31,6 @@ export function Assignment({
disableDelete,
index,
total,
- canPickColor,
- editable,
palette,
colorMode,
getPaletteFn,
@@ -48,8 +46,6 @@ export function Assignment({
disableDelete: boolean;
palette: ColorMapping.CategoricalPalette;
getPaletteFn: ReturnType;
- canPickColor: boolean;
- editable: boolean;
isDarkMode: boolean;
specialTokens: Map;
assignmentValuesCounter: Map;
@@ -57,18 +53,12 @@ export function Assignment({
const dispatch = useDispatch();
return (
-
+
{
const rule: ColorMapping.RuleRange = {
type: 'range',
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx
index 1f57e731e84c0..bfef7a270e1a0 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx
@@ -15,7 +15,6 @@ import { ColorMapping } from '../../config';
export const Match: React.FC<{
index: number;
- editable: boolean;
rule:
| ColorMapping.RuleAuto
| ColorMapping.RuleMatchExactly
@@ -25,7 +24,7 @@ export const Match: React.FC<{
options: Array;
specialTokens: Map;
assignmentValuesCounter: Map;
-}> = ({ index, rule, updateValue, editable, options, specialTokens, assignmentValuesCounter }) => {
+}> = ({ index, rule, updateValue, options, specialTokens, assignmentValuesCounter }) => {
const duplicateWarning = i18n.translate(
'coloring.colorMapping.assignments.duplicateCategoryWarning',
{
@@ -75,7 +74,6 @@ export const Match: React.FC<{
{
- if (selectedOptions.findIndex((option) => option.label.toLowerCase() === label) === -1) {
+ if (selectedOptions.findIndex((option) => option.label === label) === -1) {
updateValue([...selectedOptions, { label, value: label }].map((d) => d.value));
}
}}
+ isCaseSensitive
isClearable={false}
compressed
/>
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx
index 70f2cf49609e0..e006a7b23543f 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx
@@ -12,9 +12,8 @@ import { ColorMapping } from '../../config';
export const Range: React.FC<{
rule: ColorMapping.RuleRange;
- editable: boolean;
updateValue: (min: number, max: number, minInclusive: boolean, maxInclusive: boolean) => void;
-}> = ({ rule, updateValue, editable }) => {
+}> = ({ rule, updateValue }) => {
const minValid = rule.min <= rule.max;
const maxValid = rule.max >= rule.min;
@@ -34,7 +33,6 @@ export const Range: React.FC<{
placeholder="min"
value={rule.min}
isInvalid={!minValid}
- disabled={!editable}
onChange={(e) =>
updateValue(+e.currentTarget.value, rule.max, rule.minInclusive, rule.maxInclusive)
}
@@ -54,7 +52,6 @@ export const Range: React.FC<{
}
placeholder="max"
- disabled={!editable}
value={rule.max}
onChange={(e) =>
updateValue(rule.min, +e.currentTarget.value, rule.minInclusive, rule.maxInclusive)
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx
index 29ede59e37f41..fff892fc9cc7b 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx
@@ -6,17 +6,16 @@
* Side Public License, v 1.
*/
-import { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useDispatch } from 'react-redux';
import React from 'react';
-import { i18n } from '@kbn/i18n';
import { ColorMapping } from '../../config';
import { getPalette } from '../../palettes';
import { ColorSwatch } from '../color_picker/color_swatch';
import { updateSpecialAssignmentColor } from '../../state/color_mapping';
+import { ColorCode, CategoricalColor } from '../../config/types';
export function SpecialAssignment({
- assignment,
+ assignmentColor,
index,
palette,
getPaletteFn,
@@ -25,55 +24,31 @@ export function SpecialAssignment({
}: {
isDarkMode: boolean;
index: number;
- assignment: ColorMapping.Config['specialAssignments'][number];
+ assignmentColor: CategoricalColor | ColorCode;
palette: ColorMapping.CategoricalPalette;
getPaletteFn: ReturnType;
total: number;
}) {
const dispatch = useDispatch();
- const canPickColor = true;
return (
-
-
- {
- dispatch(
- updateSpecialAssignmentColor({
- assignmentIndex: index,
- color,
- })
- );
- }}
- />
-
-
-
-
-
+ {
+ dispatch(
+ updateSpecialAssignmentColor({
+ assignmentIndex: index,
+ color,
+ })
+ );
+ }}
+ />
);
}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx
index e1e8a08aa6b22..f576daa2096cc 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx
@@ -107,7 +107,7 @@ export function ColorPicker({
style={{ paddingBottom: 8 }}
>
{i18n.translate('coloring.colorMapping.colorPicker.removeGradientColorButtonLabel', {
- defaultMessage: 'Remove color step',
+ defaultMessage: 'Remove color stop',
})}
>
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx
index 8ddc56d2476c7..34ffbefeca30f 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx
@@ -29,11 +29,8 @@ import { getValidColor } from '../../color/color_math';
interface ColorPickerSwatchProps {
colorMode: ColorMapping.Config['colorMode'];
- assignmentColor:
- | ColorMapping.Config['assignments'][number]['color']
- | ColorMapping.Config['specialAssignments'][number]['color'];
+ assignmentColor: ColorMapping.Config['assignments'][number]['color'];
getPaletteFn: ReturnType;
- canPickColor: boolean;
index: number;
total: number;
palette: ColorMapping.CategoricalPalette;
@@ -46,7 +43,6 @@ export const ColorSwatch = ({
colorMode,
assignmentColor,
getPaletteFn,
- canPickColor,
index,
total,
palette,
@@ -71,7 +67,7 @@ export const ColorSwatch = ({
);
const colorIsDark = isColorDark(...getValidColor(colorHex).rgb());
const euiTheme = useEuiTheme();
- return canPickColor && assignmentColor.type !== 'gradient' ? (
+ return assignmentColor.type !== 'gradient' ? (
) : (
@@ -121,7 +117,7 @@ export const ColorSwatch = ({
style={{
// the color swatch can't pickup colors written in rgb/css standard
backgroundColor: colorHex,
- cursor: canPickColor ? 'pointer' : 'not-allowed',
+ cursor: 'pointer',
width: 32,
height: 32,
}}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx
index 21aa18a49f9dc..77a8273654b89 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx
@@ -35,16 +35,16 @@ export function PaletteColors({
selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void;
}) {
const colors = Array.from({ length: palette.colorCount }, (d, i) => {
- return palette.getColor(i, isDarkMode);
+ return palette.getColor(i, isDarkMode, false);
});
const neutralColors = Array.from({ length: NeutralPalette.colorCount }, (d, i) => {
- return NeutralPalette.getColor(i, isDarkMode);
+ return NeutralPalette.getColor(i, isDarkMode, false);
});
const originalColor =
color.type === 'categorical'
? color.paletteId === NeutralPalette.id
- ? NeutralPalette.getColor(color.colorIndex, isDarkMode)
- : getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)
+ ? NeutralPalette.getColor(color.colorIndex, isDarkMode, false)
+ : getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode, false)
: color.colorCode;
return (
<>
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx
index 84f6786922f44..28f155b85fe9b 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx
@@ -48,7 +48,8 @@ export function RGBPicker({
customColorMappingColor.type === 'categorical'
? getPaletteFn(customColorMappingColor.paletteId).getColor(
customColorMappingColor.colorIndex,
- isDarkMode
+ isDarkMode,
+ false
)
: customColorMappingColor.colorCode;
@@ -142,7 +143,7 @@ export function RGBPicker({
if (chromajs.valid(textColor)) {
setCustomColorMappingColor({
type: 'colorCode',
- colorCode: chromajs(textColor).hex(),
+ colorCode: textColor,
});
}
}}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx
new file mode 100644
index 0000000000000..f525311b8afb3
--- /dev/null
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx
@@ -0,0 +1,329 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiEmptyPrompt,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiIcon,
+ EuiNotificationBadge,
+ EuiPanel,
+ EuiPopover,
+ EuiText,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import React, { useCallback, useMemo, useState } from 'react';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { i18n } from '@kbn/i18n';
+import { useDispatch, useSelector } from 'react-redux';
+import { findLast } from 'lodash';
+import { Assignment } from '../assignment/assignment';
+import {
+ addNewAssignment,
+ addNewAssignments,
+ removeAllAssignments,
+} from '../../state/color_mapping';
+import { selectColorMode, selectComputedAssignments, selectPalette } from '../../state/selectors';
+import { ColorMappingInputData } from '../../categorical_color_mapping';
+import { ColorMapping } from '../../config';
+import { getPalette, NeutralPalette } from '../../palettes';
+import { ruleMatch } from '../../color/rule_matching';
+
+export function AssignmentsConfig({
+ data,
+ palettes,
+ isDarkMode,
+ specialTokens,
+}: {
+ palettes: Map;
+ data: ColorMappingInputData;
+ isDarkMode: boolean;
+ /** map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */
+ specialTokens: Map;
+}) {
+ const [showOtherActions, setShowOtherActions] = useState(false);
+
+ const dispatch = useDispatch();
+ const getPaletteFn = getPalette(palettes, NeutralPalette);
+ const palette = useSelector(selectPalette(getPaletteFn));
+ const colorMode = useSelector(selectColorMode);
+ const assignments = useSelector(selectComputedAssignments);
+
+ const unmatchingCategories = useMemo(() => {
+ return data.type === 'categories'
+ ? data.categories.filter((category) => {
+ return !assignments.some(({ rule }) => ruleMatch(rule, category));
+ })
+ : [];
+ }, [data, assignments]);
+
+ const assignmentValuesCounter = assignments.reduce>(
+ (acc, assignment) => {
+ const values = assignment.rule.type === 'matchExactly' ? assignment.rule.values : [];
+ values.forEach((value) => {
+ acc.set(value, (acc.get(value) ?? 0) + 1);
+ });
+ return acc;
+ },
+ new Map()
+ );
+
+ const onClickAddNewAssignment = useCallback(() => {
+ const lastCategorical = findLast(assignments, (d) => {
+ return d.color.type === 'categorical';
+ });
+ const nextCategoricalIndex =
+ lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0;
+ dispatch(
+ addNewAssignment({
+ rule:
+ data.type === 'categories'
+ ? {
+ type: 'matchExactly',
+ values: [],
+ }
+ : { type: 'range', min: 0, max: 0, minInclusive: true, maxInclusive: true },
+ color:
+ colorMode.type === 'categorical'
+ ? {
+ type: 'categorical',
+ paletteId: palette.id,
+ colorIndex: nextCategoricalIndex % palette.colorCount,
+ }
+ : { type: 'gradient' },
+ touched: false,
+ })
+ );
+ }, [assignments, colorMode.type, data.type, dispatch, palette.colorCount, palette.id]);
+
+ const onClickAddAllCurrentCategories = useCallback(() => {
+ if (data.type === 'categories') {
+ const lastCategorical = findLast(assignments, (d) => {
+ return d.color.type === 'categorical';
+ });
+ const nextCategoricalIndex =
+ lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0;
+
+ const newAssignments: ColorMapping.Config['assignments'] = unmatchingCategories.map(
+ (c, i) => {
+ return {
+ rule: {
+ type: 'matchExactly',
+ values: [c],
+ },
+ color:
+ colorMode.type === 'categorical'
+ ? {
+ type: 'categorical',
+ paletteId: palette.id,
+ colorIndex: (nextCategoricalIndex + i) % palette.colorCount,
+ }
+ : { type: 'gradient' },
+ touched: false,
+ };
+ }
+ );
+ dispatch(addNewAssignments(newAssignments));
+ }
+ }, [
+ dispatch,
+ assignments,
+ colorMode.type,
+ data.type,
+ palette.colorCount,
+ palette.id,
+ unmatchingCategories,
+ ]);
+
+ return (
+
+
+
+ {assignments.map((assignment, i) => {
+ return (
+
+ );
+ })}
+ {assignments.length === 0 && (
+
+
+ {i18n.translate(
+ 'coloring.colorMapping.container.mapValuesPromptDescription.mapValuesPromptDetail',
+ {
+ defaultMessage:
+ 'Add new assignments to begin associating terms in your data with specified colors.',
+ }
+ )}
+
+
+ }
+ actions={[
+
+ {i18n.translate('coloring.colorMapping.container.AddAssignmentButtonLabel', {
+ defaultMessage: 'Add assignment',
+ })}
+ ,
+
+ {i18n.translate('coloring.colorMapping.container.mapValueButtonLabel', {
+ defaultMessage: 'Add all unassigned terms',
+ })}
+ ,
+ ]}
+ />
+ )}
+
+
+ {assignments.length > 0 && }
+
+ {assignments.length > 0 && (
+
+
+ {i18n.translate('coloring.colorMapping.container.AddAssignmentButtonLabel', {
+ defaultMessage: 'Add assignment',
+ })}
+
+ {data.type === 'categories' && (
+ setShowOtherActions(true)}
+ />
+ }
+ isOpen={showOtherActions}
+ closePopover={() => setShowOtherActions(false)}
+ panelPaddingSize="xs"
+ anchorPosition="downRight"
+ ownFocus
+ >
+ {
+ setShowOtherActions(false);
+ requestAnimationFrame(() => {
+ onClickAddAllCurrentCategories();
+ });
+ }}
+ disabled={unmatchingCategories.length === 0}
+ >
+
+
+ {i18n.translate(
+ 'coloring.colorMapping.container.mapCurrentValuesButtonLabel',
+ {
+ defaultMessage: 'Add all unsassigned terms',
+ }
+ )}
+
+ {unmatchingCategories.length > 0 && (
+
+
+ {unmatchingCategories.length}
+
+
+ )}
+
+ ,
+ }
+ onClick={() => {
+ setShowOtherActions(false);
+ dispatch(removeAllAssignments());
+ }}
+ color="danger"
+ >
+ {i18n.translate(
+ 'coloring.colorMapping.container.clearAllAssignmentsButtonLabel',
+ {
+ defaultMessage: 'Clear all assignments',
+ }
+ )}
+ ,
+ ]}
+ />
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx
index 748f17fa45842..e3de3c0a24261 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx
@@ -8,56 +8,28 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormLabel,
- EuiHorizontalRule,
- EuiPanel,
- EuiSwitch,
- EuiText,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
-import { Assignment } from '../assignment/assignment';
-import { SpecialAssignment } from '../assignment/special_assignment';
import { PaletteSelector } from '../palette_selector/palette_selector';
-import {
- RootState,
- addNewAssignment,
- assignAutomatically,
- assignStatically,
- changeGradientSortOrder,
-} from '../../state/color_mapping';
-import { generateAutoAssignmentsForCategories } from '../../config/assignment_from_categories';
+import { changeGradientSortOrder } from '../../state/color_mapping';
import { ColorMapping } from '../../config';
import { getPalette } from '../../palettes';
-import { getUnusedColorForNewAssignment } from '../../config/assignments';
-import {
- selectColorMode,
- selectPalette,
- selectSpecialAssignments,
- selectIsAutoAssignmentMode,
-} from '../../state/selectors';
+import { selectColorMode, selectComputedAssignments, selectPalette } from '../../state/selectors';
import { ColorMappingInputData } from '../../categorical_color_mapping';
import { Gradient } from '../palette_selector/gradient';
import { NeutralPalette } from '../../palettes/neutral';
+import { ScaleMode } from '../palette_selector/scale';
+import { UnassignedTermsConfig } from './unassigned_terms_config';
+import { AssignmentsConfig } from './assigments';
-export const MAX_ASSIGNABLE_COLORS = 10;
-
-function selectComputedAssignments(
- data: ColorMappingInputData,
- palette: ColorMapping.CategoricalPalette,
- colorMode: ColorMapping.Config['colorMode']
-) {
- return (state: RootState) =>
- state.colorMapping.assignmentMode === 'auto'
- ? generateAutoAssignmentsForCategories(data, palette, colorMode)
- : state.colorMapping.assignments;
-}
-export function Container(props: {
+export function Container({
+ data,
+ palettes,
+ isDarkMode,
+ specialTokens,
+}: {
palettes: Map;
data: ColorMappingInputData;
isDarkMode: boolean;
@@ -66,187 +38,99 @@ export function Container(props: {
}) {
const dispatch = useDispatch();
- const getPaletteFn = getPalette(props.palettes, NeutralPalette);
+ const getPaletteFn = getPalette(palettes, NeutralPalette);
const palette = useSelector(selectPalette(getPaletteFn));
const colorMode = useSelector(selectColorMode);
- const autoAssignmentMode = useSelector(selectIsAutoAssignmentMode);
- const assignments = useSelector(selectComputedAssignments(props.data, palette, colorMode));
- const specialAssignments = useSelector(selectSpecialAssignments);
-
- const canAddNewAssignment = !autoAssignmentMode && assignments.length < MAX_ASSIGNABLE_COLORS;
-
- const assignmentValuesCounter = assignments.reduce>(
- (acc, assignment) => {
- const values = assignment.rule.type === 'matchExactly' ? assignment.rule.values : [];
- values.forEach((value) => {
- acc.set(value, (acc.get(value) ?? 0) + 1);
- });
- return acc;
- },
- new Map()
- );
+ const assignments = useSelector(selectComputedAssignments);
return (
-
-
-
-
+
-
+
-
- {i18n.translate('coloring.colorMapping.container.mappingAssignmentHeader', {
- defaultMessage: 'Mapping assignments',
- })}
-
+
-
- {i18n.translate('coloring.colorMapping.container.autoAssignLabel', {
- defaultMessage: 'Auto assign',
- })}
-
- }
- checked={autoAssignmentMode}
- compressed
- onChange={() => {
- if (autoAssignmentMode) {
- dispatch(assignStatically(assignments));
- } else {
- dispatch(assignAutomatically());
- }
- }}
- />
+
-
-
+ {colorMode.type === 'gradient' && (
+
- {colorMode.type !== 'gradient' ? null : (
-
- )}
- {assignments.map((assignment, i) => {
- return (
-
- );
- })}
-
-
-
-
-
- {props.data.type === 'categories' &&
- specialAssignments.map((assignment, i) => {
- return (
-
- );
- })}
-
-
-
-
-
- {
- dispatch(
- addNewAssignment({
- rule:
- props.data.type === 'categories'
- ? {
- type: 'matchExactly',
- values: [],
- }
- : { type: 'range', min: 0, max: 0, minInclusive: true, maxInclusive: true },
- color: getUnusedColorForNewAssignment(palette, colorMode, assignments),
- touched: false,
- })
- );
- }}
- disabled={!canAddNewAssignment}
- css={css`
- margin-right: 8px;
- `}
- >
- {i18n.translate('coloring.colorMapping.container.addAssignmentButtonLabel', {
- defaultMessage: 'Add assignment',
+
- {colorMode.type === 'gradient' && (
-
+ {
dispatch(changeGradientSortOrder(colorMode.sort === 'asc' ? 'desc' : 'asc'));
}}
- >
- {i18n.translate('coloring.colorMapping.container.invertGradientButtonLabel', {
- defaultMessage: 'Invert gradient',
- })}
-
- )}
-
-
+ />
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ {assignments.length > 0 && (
+
+
+
+ )}
);
}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx
new file mode 100644
index 0000000000000..8e90bcc38d119
--- /dev/null
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx
@@ -0,0 +1,150 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+
+import {
+ EuiButtonGroup,
+ EuiButtonGroupOptionProps,
+ EuiColorPickerSwatch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useDispatch, useSelector } from 'react-redux';
+import { css } from '@emotion/react';
+import { updateSpecialAssignmentColor } from '../../state/color_mapping';
+import { getPalette, NeutralPalette } from '../../palettes';
+import {
+ DEFAULT_NEUTRAL_PALETTE_INDEX,
+ DEFAULT_OTHER_ASSIGNMENT_INDEX,
+} from '../../config/default_color_mapping';
+import { SpecialAssignment } from '../assignment/special_assignment';
+import { ColorMapping } from '../../config';
+import { selectColorMode, selectPalette, selectSpecialAssignments } from '../../state/selectors';
+import { ColorMappingInputData } from '../../categorical_color_mapping';
+
+export function UnassignedTermsConfig({
+ palettes,
+ data,
+ isDarkMode,
+}: {
+ palettes: Map;
+ data: ColorMappingInputData;
+ isDarkMode: boolean;
+}) {
+ const dispatch = useDispatch();
+
+ const getPaletteFn = getPalette(palettes, NeutralPalette);
+
+ const palette = useSelector(selectPalette(getPaletteFn));
+ const colorMode = useSelector(selectColorMode);
+ const specialAssignments = useSelector(selectSpecialAssignments);
+ const otherAssignment = specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX];
+
+ const colorModes: EuiButtonGroupOptionProps[] = [
+ {
+ id: 'loop',
+ label:
+ colorMode.type === 'gradient'
+ ? i18n.translate(
+ 'coloring.colorMapping.container.unassignedTermsMode.ReuseGradientLabel',
+ {
+ defaultMessage: 'Gradient',
+ }
+ )
+ : i18n.translate('coloring.colorMapping.container.unassignedTermsMode.ReuseColorsLabel', {
+ defaultMessage: 'Color palette',
+ }),
+ },
+ {
+ id: 'static',
+ label: i18n.translate(
+ 'coloring.colorMapping.container.unassignedTermsMode.SingleColorLabel',
+ {
+ defaultMessage: 'Single color',
+ }
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ {
+ dispatch(
+ updateSpecialAssignmentColor({
+ assignmentIndex: 0,
+ color:
+ optionId === 'loop'
+ ? {
+ type: 'loop',
+ }
+ : {
+ type: 'categorical',
+ colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX,
+ paletteId: NeutralPalette.id,
+ },
+ })
+ );
+ }}
+ buttonSize="compressed"
+ isFullWidth
+ />
+
+
+
+ {data.type === 'categories' && otherAssignment.color.type !== 'loop' ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx
index c4e22f797deaa..99eda60166ac4 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx
@@ -6,331 +6,174 @@
* Side Public License, v 1.
*/
-import { euiFocusRing, EuiIcon, euiShadowSmall, useEuiTheme } from '@elastic/eui';
import React from 'react';
-import { useDispatch } from 'react-redux';
-
import { euiThemeVars } from '@kbn/ui-theme';
import { css } from '@emotion/react';
+import { useDispatch } from 'react-redux';
import { changeAlpha } from '../../color/color_math';
-
import { ColorMapping } from '../../config';
-import { ColorSwatch } from '../color_picker/color_swatch';
import { getPalette } from '../../palettes';
-
-import { addGradientColorStep, updateGradientColorStep } from '../../state/color_mapping';
-import { colorPickerVisibility } from '../../state/ui';
import { getGradientColorScale } from '../../color/color_handling';
+import { AddStop } from './gradient_add_stop';
+import { ColorSwatch } from '../color_picker/color_swatch';
+import { updateGradientColorStep } from '../../state/color_mapping';
export function Gradient({
paletteId,
colorMode,
getPaletteFn,
isDarkMode,
- assignmentsSize,
}: {
paletteId: string;
isDarkMode: boolean;
colorMode: ColorMapping.Config['colorMode'];
getPaletteFn: ReturnType;
- assignmentsSize: number;
}) {
+ const dispatch = useDispatch();
if (colorMode.type === 'categorical') {
return null;
}
+
const currentPalette = getPaletteFn(paletteId);
const gradientColorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode);
- const topMostColorStop =
+ const startStepColor =
colorMode.sort === 'asc'
? colorMode.steps.length === 1
? undefined
: colorMode.steps.at(-1)
: colorMode.steps.at(0);
- const topMostColorStopIndex =
+ const startStepIndex =
colorMode.sort === 'asc'
? colorMode.steps.length === 1
? NaN
: colorMode.steps.length - 1
: 0;
- const bottomMostColorStop =
+ const endStepColor =
colorMode.sort === 'asc'
? colorMode.steps.at(0)
: colorMode.steps.length === 1
? undefined
: colorMode.steps.at(-1);
- const bottomMostColorStopIndex =
+ const endStepIndex =
colorMode.sort === 'asc' ? 0 : colorMode.steps.length === 1 ? NaN : colorMode.steps.length - 1;
- const middleMostColorSep = colorMode.steps.length === 3 ? colorMode.steps[1] : undefined;
- const middleMostColorStopIndex = colorMode.steps.length === 3 ? 1 : NaN;
+ const middleStepColor = colorMode.steps.length === 3 ? colorMode.steps[1] : undefined;
+ const middleStepIndex = colorMode.steps.length === 3 ? 1 : NaN;
return (
- <>
- {assignmentsSize > 1 && (
-
- )}
+
+
+
- {topMostColorStop ? (
-
{
+ dispatch(updateGradientColorStep({ index: startStepIndex, color }));
+ }}
/>
) : (
)}
- {assignmentsSize > 1 && (
-
-
- {middleMostColorSep ? (
-
- ) : colorMode.steps.length === 2 ? (
-
- ) : undefined}
-
-
- )}
- {assignmentsSize > 1 && (
-
- )}
-
- {bottomMostColorStop ? (
-
{
+ dispatch(updateGradientColorStep({ index: middleStepIndex, color }));
+ }}
/>
- ) : (
+ ) : colorMode.steps.length === 2 ? (
- )}
+ ) : undefined}
- >
- );
-}
-
-function AddStop({
- colorMode,
- currentPalette,
- at,
-}: {
- colorMode: {
- type: 'gradient';
- steps: Array<(ColorMapping.CategoricalColor | ColorMapping.ColorCode) & { touched: boolean }>;
- };
- currentPalette: ColorMapping.CategoricalPalette;
- at: number;
-}) {
- const euiTheme = useEuiTheme();
- const dispatch = useDispatch();
- return (
-
{
- dispatch(
- addGradientColorStep({
- color: {
- type: 'categorical',
- // TODO assign the next available color or a better one
- colorIndex: colorMode.steps.length,
- paletteId: currentPalette.id,
- },
- at,
- })
- );
- dispatch(
- colorPickerVisibility({
- index: at,
- type: 'gradient',
- visible: true,
- })
- );
- }}
- >
-
+ {endStepColor ? (
+
{
+ dispatch(updateGradientColorStep({ index: endStepIndex, color }));
+ }}
+ />
+ ) : (
+
+ )}
-
- );
-}
-
-function ColorStop({
- colorMode,
- step,
- index,
- currentPalette,
- getPaletteFn,
- isDarkMode,
-}: {
- colorMode: ColorMapping.GradientColorMode;
- step: ColorMapping.CategoricalColor | ColorMapping.ColorCode;
- index: number;
- currentPalette: ColorMapping.CategoricalPalette;
- getPaletteFn: ReturnType
;
- isDarkMode: boolean;
-}) {
- const dispatch = useDispatch();
- return (
- {
- dispatch(
- updateGradientColorStep({
- index,
- color,
- })
- );
- }}
- forType="gradient"
- />
+
);
}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx
new file mode 100644
index 0000000000000..713e780bc32da
--- /dev/null
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import React from 'react';
+
+import {
+ euiCanAnimate,
+ euiFocusRing,
+ EuiIcon,
+ euiShadowSmall,
+ EuiToolTip,
+ useEuiTheme,
+} from '@elastic/eui';
+import { useDispatch } from 'react-redux';
+import { i18n } from '@kbn/i18n';
+import { css } from '@emotion/react';
+import { ColorMapping } from '../../config';
+import { addGradientColorStep } from '../../state/color_mapping';
+import { colorPickerVisibility } from '../../state/ui';
+
+export function AddStop({
+ colorMode,
+ currentPalette,
+ at,
+}: {
+ colorMode: ColorMapping.GradientColorMode;
+ currentPalette: ColorMapping.CategoricalPalette;
+ at: number;
+}) {
+ const euiTheme = useEuiTheme();
+ const dispatch = useDispatch();
+ return (
+ <>
+
+ {
+ dispatch(
+ addGradientColorStep({
+ color: {
+ type: 'categorical',
+ colorIndex: colorMode.steps.length,
+ paletteId: currentPalette.id,
+ },
+ at,
+ })
+ );
+ dispatch(
+ colorPickerVisibility({
+ index: at,
+ type: 'gradient',
+ visible: true,
+ })
+ );
+ }}
+ >
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx
index c9fab3526a786..3db54cea6b108 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx
@@ -8,14 +8,7 @@
import React, { useCallback, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import {
- EuiButtonGroup,
- EuiColorPalettePicker,
- EuiConfirmModal,
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormRow,
-} from '@elastic/eui';
+import { EuiColorPalettePicker, EuiConfirmModal, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RootState, updatePalette } from '../../state/color_mapping';
@@ -33,11 +26,8 @@ export function PaletteSelector({
isDarkMode: boolean;
}) {
const dispatch = useDispatch();
- const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode);
const model = useSelector((state: RootState) => state.colorMapping);
- const { paletteId } = model;
-
const switchPaletteFn = useCallback(
(selectedPaletteId: string, preserveColorChanges: boolean) => {
dispatch(
@@ -45,7 +35,6 @@ export function PaletteSelector({
paletteId: selectedPaletteId,
assignments: updateAssignmentsPalette(
model.assignments,
- model.assignmentMode,
model.colorMode,
selectedPaletteId,
getPaletteFn,
@@ -62,37 +51,6 @@ export function PaletteSelector({
[getPaletteFn, model, dispatch]
);
- const updateColorMode = useCallback(
- (type: 'gradient' | 'categorical', preserveColorChanges: boolean) => {
- const updatedColorMode: ColorMapping.Config['colorMode'] =
- type === 'gradient'
- ? {
- type: 'gradient',
- steps: [
- {
- type: 'categorical',
- paletteId,
- colorIndex: 0,
- touched: false,
- },
- ],
- sort: 'desc',
- }
- : { type: 'categorical' };
-
- const assignments = updateAssignmentsPalette(
- model.assignments,
- model.assignmentMode,
- updatedColorMode,
- paletteId,
- getPaletteFn,
- preserveColorChanges
- );
- dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode }));
- },
- [getPaletteFn, model, dispatch, paletteId]
- );
-
const [preserveModalPaletteId, setPreserveModalPaletteId] = useState(null);
const preserveChangesModal =
@@ -126,136 +84,44 @@ export function PaletteSelector({
) : null;
- const [colorScaleModalId, setColorScaleModalId] = useState<'gradient' | 'categorical' | null>(
- null
- );
-
- const colorScaleModal =
- colorScaleModalId !== null ? (
- {
- setColorScaleModalId(null);
- }}
- onConfirm={() => {
- if (colorScaleModalId) updateColorMode(colorScaleModalId, false);
- setColorScaleModalId(null);
- }}
- cancelButtonText={i18n.translate(
- 'coloring.colorMapping.colorChangesModal.goBackButtonLabel',
- {
- defaultMessage: 'Go back',
- }
- )}
- confirmButtonText={i18n.translate(
- 'coloring.colorMapping.colorChangesModal.discardButtonLabel',
- {
- defaultMessage: 'Discard changes',
- }
- )}
- defaultFocusedButton="confirm"
- buttonColor="danger"
- >
-
- {colorScaleModalId === 'categorical'
- ? i18n.translate('coloring.colorMapping.colorChangesModal.categoricalModeDescription', {
- defaultMessage: `Switching to a categorical mode will discard all your custom color changes`,
- })
- : i18n.translate('coloring.colorMapping.colorChangesModal.sequentialModeDescription', {
- defaultMessage: `Switching to a sequential mode will discard all your custom color changes`,
- })}
-
-
- ) : null;
-
return (
<>
{preserveChangesModal}
- {colorScaleModal}
-
-
-
- d.name !== 'Neutral')
- .map((palette) => ({
- 'data-test-subj': `kbnColoring_ColorMapping_Palette-${palette.id}`,
- value: palette.id,
- title: palette.name,
- palette: Array.from({ length: palette.colorCount }, (_, i) => {
- return palette.getColor(i, isDarkMode);
- }),
- type: 'fixed',
- }))}
- onChange={(selectedPaletteId) => {
- const hasChanges = model.assignments.some((a) => a.touched);
- const hasGradientChanges =
- model.colorMode.type === 'gradient' &&
- model.colorMode.steps.some((a) => a.touched);
- if (hasChanges || hasGradientChanges) {
- setPreserveModalPaletteId(selectedPaletteId);
- } else {
- switchPaletteFn(selectedPaletteId, false);
- }
- }}
- valueOfSelected={model.paletteId}
- selectionDisplay={'palette'}
- compressed={true}
- />
-
-
-
-
- {
- const hasChanges = model.assignments.some((a) => a.touched);
- const hasGradientChanges =
- model.colorMode.type === 'gradient' &&
- model.colorMode.steps.some((a) => a.touched);
-
- if (hasChanges || hasGradientChanges) {
- setColorScaleModalId(id as 'gradient' | 'categorical');
- } else {
- updateColorMode(id as 'gradient' | 'categorical', false);
- }
- }}
- isIconOnly
- />
-
-
-
+
+ d.name !== 'Neutral')
+ .map((palette) => ({
+ 'data-test-subj': `kbnColoring_ColorMapping_Palette-${palette.id}`,
+ value: palette.id,
+ title: palette.name,
+ palette: Array.from({ length: palette.colorCount }, (_, i) => {
+ return palette.getColor(i, isDarkMode, false);
+ }),
+ type: 'fixed',
+ }))}
+ onChange={(selectedPaletteId) => {
+ const hasChanges = model.assignments.some((a) => a.touched);
+ const hasGradientChanges =
+ model.colorMode.type === 'gradient' && model.colorMode.steps.some((a) => a.touched);
+ if (hasChanges || hasGradientChanges) {
+ setPreserveModalPaletteId(selectedPaletteId);
+ } else {
+ switchPaletteFn(selectedPaletteId, false);
+ }
+ }}
+ valueOfSelected={model.paletteId}
+ selectionDisplay={'palette'}
+ compressed={true}
+ />
+
>
);
}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx
new file mode 100644
index 0000000000000..056db47157c60
--- /dev/null
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useCallback, useState } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { EuiButtonGroup, EuiConfirmModal, EuiFormRow } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { RootState, updatePalette } from '../../state/color_mapping';
+import { ColorMapping } from '../../config';
+import { updateAssignmentsPalette } from '../../config/assignments';
+import { getPalette } from '../../palettes';
+
+export function ScaleMode({ getPaletteFn }: { getPaletteFn: ReturnType }) {
+ const dispatch = useDispatch();
+ const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode);
+ const model = useSelector((state: RootState) => state.colorMapping);
+
+ const { paletteId } = model;
+
+ const updateColorMode = useCallback(
+ (type: 'gradient' | 'categorical', preserveColorChanges: boolean) => {
+ const updatedColorMode: ColorMapping.Config['colorMode'] =
+ type === 'gradient'
+ ? {
+ type: 'gradient',
+ steps: [
+ {
+ type: 'categorical',
+ paletteId,
+ colorIndex: 0,
+ touched: false,
+ },
+ ],
+ sort: 'desc',
+ }
+ : { type: 'categorical' };
+
+ const assignments = updateAssignmentsPalette(
+ model.assignments,
+ updatedColorMode,
+ paletteId,
+ getPaletteFn,
+ preserveColorChanges
+ );
+ dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode }));
+ },
+ [getPaletteFn, model, dispatch, paletteId]
+ );
+
+ const [colorScaleModalId, setColorScaleModalId] = useState<'gradient' | 'categorical' | null>(
+ null
+ );
+
+ const colorScaleModal =
+ colorScaleModalId !== null ? (
+ {
+ setColorScaleModalId(null);
+ }}
+ onConfirm={() => {
+ if (colorScaleModalId) updateColorMode(colorScaleModalId, false);
+ setColorScaleModalId(null);
+ }}
+ cancelButtonText={i18n.translate(
+ 'coloring.colorMapping.colorChangesModal.goBackButtonLabel',
+ {
+ defaultMessage: 'Go back',
+ }
+ )}
+ confirmButtonText={i18n.translate(
+ 'coloring.colorMapping.colorChangesModal.discardButtonLabel',
+ {
+ defaultMessage: 'Discard changes',
+ }
+ )}
+ defaultFocusedButton="confirm"
+ buttonColor="danger"
+ >
+
+ {colorScaleModalId === 'categorical'
+ ? i18n.translate('coloring.colorMapping.colorChangesModal.categoricalModeDescription', {
+ defaultMessage: `Switching to a categorical mode will discard all your custom color changes`,
+ })
+ : i18n.translate('coloring.colorMapping.colorChangesModal.sequentialModeDescription', {
+ defaultMessage: `Switching to a gradient mode will discard all your custom color changes`,
+ })}
+
+
+ ) : null;
+
+ return (
+ <>
+ {colorScaleModal}
+
+ {
+ const hasChanges = model.assignments.some((a) => a.touched);
+ const hasGradientChanges =
+ model.colorMode.type === 'gradient' && model.colorMode.steps.some((a) => a.touched);
+
+ if (hasChanges || hasGradientChanges) {
+ setColorScaleModalId(id as 'gradient' | 'categorical');
+ } else {
+ updateColorMode(id as 'gradient' | 'categorical', false);
+ }
+ }}
+ />
+
+ >
+ );
+}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts
deleted file mode 100644
index 97c4d17c35e4d..0000000000000
--- a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { ColorMapping } from '.';
-import { ColorMappingInputData } from '../categorical_color_mapping';
-import { MAX_ASSIGNABLE_COLORS } from '../components/container/container';
-
-export function generateAutoAssignmentsForCategories(
- data: ColorMappingInputData,
- palette: ColorMapping.CategoricalPalette,
- colorMode: ColorMapping.Config['colorMode']
-): ColorMapping.Config['assignments'] {
- const isCategorical = colorMode.type === 'categorical';
-
- const maxColorAssignable = data.type === 'categories' ? data.categories.length : data.bins;
-
- const assignableColors = isCategorical
- ? Math.min(palette.colorCount, maxColorAssignable)
- : Math.min(MAX_ASSIGNABLE_COLORS, maxColorAssignable);
-
- const autoRules: Array =
- data.type === 'categories'
- ? data.categories.map((c) => ({ type: 'matchExactly', values: [c] }))
- : Array.from({ length: data.bins }, (d, i) => {
- const step = (data.max - data.min) / data.bins;
- return {
- type: 'range',
- min: data.max - i * step - step,
- max: data.max - i * step,
- minInclusive: true,
- maxInclusive: false,
- };
- });
-
- const assignments = autoRules
- .slice(0, assignableColors)
- .map((rule, colorIndex) => {
- if (isCategorical) {
- return {
- rule,
- color: {
- type: 'categorical',
- paletteId: palette.id,
- colorIndex,
- },
- touched: false,
- };
- } else {
- return {
- rule,
- color: {
- type: 'gradient',
- },
- touched: false,
- };
- }
- });
-
- return assignments;
-}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts
index 701baa1b1710b..ce21732122150 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts
@@ -7,41 +7,35 @@
*/
import type { ColorMapping } from '.';
-import { MAX_ASSIGNABLE_COLORS } from '../components/container/container';
-import { getPalette, NeutralPalette } from '../palettes';
-import { DEFAULT_NEUTRAL_PALETTE_INDEX } from './default_color_mapping';
+import { getPalette } from '../palettes';
export function updateAssignmentsPalette(
assignments: ColorMapping.Config['assignments'],
- assignmentMode: ColorMapping.Config['assignmentMode'],
colorMode: ColorMapping.Config['colorMode'],
paletteId: string,
getPaletteFn: ReturnType,
preserveColorChanges: boolean
): ColorMapping.Config['assignments'] {
const palette = getPaletteFn(paletteId);
- const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS;
- return assignmentMode === 'auto'
- ? []
- : assignments.map(({ rule, color, touched }, index) => {
- if (preserveColorChanges && touched) {
- return { rule, color, touched };
- } else {
- const newColor: ColorMapping.Config['assignments'][number]['color'] =
- colorMode.type === 'categorical'
- ? {
- type: 'categorical',
- paletteId: index < maxColors ? paletteId : NeutralPalette.id,
- colorIndex: index < maxColors ? index : 0,
- }
- : { type: 'gradient' };
- return {
- rule,
- color: newColor,
- touched: false,
- };
- }
- });
+ return assignments.map(({ rule, color, touched }, index) => {
+ if (preserveColorChanges && touched) {
+ return { rule, color, touched };
+ } else {
+ const newColor: ColorMapping.Config['assignments'][number]['color'] =
+ colorMode.type === 'categorical'
+ ? {
+ type: 'categorical',
+ paletteId,
+ colorIndex: index % palette.colorCount,
+ }
+ : { type: 'gradient' };
+ return {
+ rule,
+ color: newColor,
+ touched: false,
+ };
+ }
+ });
}
export function updateColorModePalette(
@@ -61,31 +55,3 @@ export function updateColorModePalette(
sort: colorMode.sort,
};
}
-
-export function getUnusedColorForNewAssignment(
- palette: ColorMapping.CategoricalPalette,
- colorMode: ColorMapping.Config['colorMode'],
- assignments: ColorMapping.Config['assignments']
-): ColorMapping.Config['assignments'][number]['color'] {
- if (colorMode.type === 'categorical') {
- // TODO: change the type of color assignment depending on palette
- // compute the next unused color index in the palette.
- const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS;
- const colorIndices = new Set(Array.from({ length: maxColors }, (d, i) => i));
- assignments.forEach(({ color }) => {
- if (color.type === 'categorical' && color.paletteId === palette.id) {
- colorIndices.delete(color.colorIndex);
- }
- });
- const paletteForNextUnusedColorIndex = colorIndices.size > 0 ? palette.id : NeutralPalette.id;
- const nextUnusedColorIndex =
- colorIndices.size > 0 ? [...colorIndices][0] : DEFAULT_NEUTRAL_PALETTE_INDEX;
- return {
- type: 'categorical',
- paletteId: paletteForNextUnusedColorIndex,
- colorIndex: nextUnusedColorIndex,
- };
- } else {
- return { type: 'gradient' };
- }
-}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts
index e4005770b2883..8a6ae646b7b6b 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts
@@ -13,12 +13,12 @@ import { NeutralPalette } from '../palettes/neutral';
import { getColor, getGradientColorScale } from '../color/color_handling';
export const DEFAULT_NEUTRAL_PALETTE_INDEX = 1;
+export const DEFAULT_OTHER_ASSIGNMENT_INDEX = 0;
/**
* The default color mapping used in Kibana, starts with the EUI color palette
*/
export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = {
- assignmentMode: 'auto',
assignments: [],
specialAssignments: [
{
@@ -26,9 +26,7 @@ export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = {
type: 'other',
},
color: {
- type: 'categorical',
- paletteId: NeutralPalette.id,
- colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX,
+ type: 'loop',
},
touched: false,
},
@@ -45,17 +43,26 @@ export function getPaletteColors(
): string[] {
const colorMappingModel = colorMappings ?? { ...DEFAULT_COLOR_MAPPING_CONFIG };
const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(colorMappingModel.paletteId);
- return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode));
+ return getPaletteColorsFromPaletteId(isDarkMode, palette.id);
+}
+
+export function getPaletteColorsFromPaletteId(
+ isDarkMode: boolean,
+ paletteId: ColorMapping.Config['paletteId']
+): string[] {
+ const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(paletteId);
+ return Array.from({ length: palette.colorCount }, (d, i) =>
+ palette.getColor(i, isDarkMode, true)
+ );
}
export function getColorsFromMapping(
isDarkMode: boolean,
colorMappings?: ColorMapping.Config
): string[] {
- const { colorMode, paletteId, assignmentMode, assignments, specialAssignments } =
- colorMappings ?? {
- ...DEFAULT_COLOR_MAPPING_CONFIG,
- };
+ const { colorMode, paletteId, assignments, specialAssignments } = colorMappings ?? {
+ ...DEFAULT_COLOR_MAPPING_CONFIG,
+ };
const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette);
if (colorMode.type === 'gradient') {
@@ -63,17 +70,23 @@ export function getColorsFromMapping(
return Array.from({ length: 6 }, (d, i) => colorScale(i / 6));
} else {
const palette = getPaletteFn(paletteId);
- if (assignmentMode === 'auto') {
- return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode));
- } else {
- return [
- ...assignments.map((a) => {
- return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode);
- }),
- ...specialAssignments.map((a) => {
- return getColor(a.color, getPaletteFn, isDarkMode);
- }),
- ].filter((color) => color !== '');
- }
+ const otherColors =
+ specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color.type === 'loop'
+ ? Array.from({ length: palette.colorCount }, (d, i) =>
+ palette.getColor(i, isDarkMode, true)
+ )
+ : [
+ getColor(
+ specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color,
+ getPaletteFn,
+ isDarkMode
+ ),
+ ];
+ return [
+ ...assignments.map((a) => {
+ return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode);
+ }),
+ ...otherColors,
+ ].filter((color) => color !== '');
}
}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts
index 59cb18435112d..4c62044be9242 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts
@@ -30,6 +30,13 @@ export interface GradientColor {
type: 'gradient';
}
+/**
+ * An index specified categorical color, coming from paletteId
+ */
+export interface LoopColor {
+ type: 'loop';
+}
+
/**
* A special rule that match automatically, in order, all the categories that are not matching a specified rule
*/
@@ -134,14 +141,13 @@ export interface GradientColorMode {
export interface Config {
paletteId: string;
colorMode: CategoricalColorMode | GradientColorMode;
- assignmentMode: 'auto' | 'manual';
assignments: Array<
Assignment<
RuleAuto | RuleMatchExactly | RuleMatchExactlyCI | RuleRange | RuleRegExp,
CategoricalColor | ColorCode | GradientColor
>
>;
- specialAssignments: Array>;
+ specialAssignments: Array>;
}
export interface CategoricalPalette {
@@ -149,5 +155,5 @@ export interface CategoricalPalette {
name: string;
type: 'categorical';
colorCount: number;
- getColor: (valueInRange: number, isDarkMode: boolean) => string;
+ getColor: (valueInRange: number, isDarkMode: boolean, loop: boolean) => string;
}
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts
index 1b49a2c6a8bf3..7484eabe816ab 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/index.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts
@@ -14,6 +14,7 @@ export * from './color/color_handling';
export { SPECIAL_TOKENS_STRING_CONVERTION } from './color/rule_matching';
export {
DEFAULT_COLOR_MAPPING_CONFIG,
+ DEFAULT_OTHER_ASSIGNMENT_INDEX,
getPaletteColors,
getColorsFromMapping,
} from './config/default_color_mapping';
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts
index d93440c5ac5e4..ce13184ff062a 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts
@@ -22,7 +22,9 @@ export const ElasticBrandPalette: ColorMapping.CategoricalPalette = {
name: 'Elastic Brand',
colorCount: ELASTIC_BRAND_PALETTE_COLORS.length,
type: 'categorical',
- getColor(valueInRange) {
- return ELASTIC_BRAND_PALETTE_COLORS[valueInRange];
+ getColor(indexInRange, isDarkMode, loop) {
+ return ELASTIC_BRAND_PALETTE_COLORS[
+ loop ? indexInRange % ELASTIC_BRAND_PALETTE_COLORS.length : indexInRange
+ ];
},
};
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts
index ec48793e12819..f9836a400b877 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts
@@ -26,7 +26,9 @@ export const EUIAmsterdamColorBlindPalette: ColorMapping.CategoricalPalette = {
name: 'Default',
colorCount: EUI_AMSTERDAM_PALETTE_COLORS.length,
type: 'categorical',
- getColor(valueInRange) {
- return EUI_AMSTERDAM_PALETTE_COLORS[valueInRange];
+ getColor(indexInRange, isDarkMode, loop) {
+ return EUI_AMSTERDAM_PALETTE_COLORS[
+ loop ? indexInRange % EUI_AMSTERDAM_PALETTE_COLORS.length : indexInRange
+ ];
},
};
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts
index 9b576e0b05c66..bb90130a817fe 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts
@@ -23,7 +23,9 @@ export const KibanaV7LegacyPalette: ColorMapping.CategoricalPalette = {
name: 'Kibana Legacy',
colorCount: KIBANA_V7_LEGACY_PALETTE_COLORS.length,
type: 'categorical',
- getColor(valueInRange) {
- return KIBANA_V7_LEGACY_PALETTE_COLORS[valueInRange];
+ getColor(indexInRange, isDarkMode, loop) {
+ return KIBANA_V7_LEGACY_PALETTE_COLORS[
+ loop ? indexInRange % KIBANA_V7_LEGACY_PALETTE_COLORS.length : indexInRange
+ ];
},
};
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts
index 27588aff2b389..704dbedcfec23 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts
@@ -9,6 +9,7 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { ColorMapping } from '../config';
+import { DEFAULT_OTHER_ASSIGNMENT_INDEX } from '../config/default_color_mapping';
export interface RootState {
colorMapping: ColorMapping.Config;
@@ -22,7 +23,6 @@ export interface RootState {
}
const initialState: RootState['colorMapping'] = {
- assignmentMode: 'auto',
assignments: [],
specialAssignments: [],
paletteId: 'eui',
@@ -34,7 +34,6 @@ export const colorMappingSlice = createSlice({
initialState,
reducers: {
updateModel: (state, action: PayloadAction) => {
- state.assignmentMode = action.payload.assignmentMode;
state.assignments = [...action.payload.assignments];
state.specialAssignments = [...action.payload.specialAssignments];
state.paletteId = action.payload.paletteId;
@@ -53,11 +52,9 @@ export const colorMappingSlice = createSlice({
state.colorMode = { ...action.payload.colorMode };
},
assignStatically: (state, action: PayloadAction) => {
- state.assignmentMode = 'manual';
state.assignments = [...action.payload];
},
assignAutomatically: (state) => {
- state.assignmentMode = 'auto';
state.assignments = [];
},
@@ -67,6 +64,9 @@ export const colorMappingSlice = createSlice({
) => {
state.assignments.push({ ...action.payload });
},
+ addNewAssignments: (state, action: PayloadAction) => {
+ state.assignments.push(...action.payload);
+ },
updateAssignment: (
state,
action: PayloadAction<{
@@ -120,6 +120,21 @@ export const colorMappingSlice = createSlice({
},
removeAssignment: (state, action: PayloadAction) => {
state.assignments.splice(action.payload, 1);
+ if (state.assignments.length === 0) {
+ state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX] = {
+ ...state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX],
+ color: { type: 'loop' },
+ touched: true,
+ };
+ }
+ },
+ removeAllAssignments: (state) => {
+ state.assignments = [];
+ state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX] = {
+ ...state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX],
+ color: { type: 'loop' },
+ touched: true,
+ };
},
changeColorMode: (state, action: PayloadAction) => {
state.colorMode = { ...action.payload };
@@ -209,11 +224,13 @@ export const {
assignStatically,
assignAutomatically,
addNewAssignment,
+ addNewAssignments,
updateAssignment,
updateAssignmentColor,
updateSpecialAssignmentColor,
updateAssignmentRule,
removeAssignment,
+ removeAllAssignments,
changeColorMode,
updateGradientColorStep,
removeGradientColorStep,
diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts
index 69bd57d2d852e..07cfdb9af0a79 100644
--- a/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts
+++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts
@@ -18,9 +18,9 @@ export function selectColorMode(state: RootState) {
export function selectSpecialAssignments(state: RootState) {
return state.colorMapping.specialAssignments;
}
-export function selectIsAutoAssignmentMode(state: RootState) {
- return state.colorMapping.assignmentMode === 'auto';
-}
export function selectColorPickerVisibility(state: RootState) {
return state.ui.colorPicker;
}
+export function selectComputedAssignments(state: RootState) {
+ return state.colorMapping.assignments;
+}
diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts
index d6a6701aa658a..7998ba5bda3c0 100644
--- a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts
+++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts
@@ -10,35 +10,17 @@ import {
ColorMapping,
EUIAmsterdamColorBlindPalette,
ElasticBrandPalette,
- NeutralPalette,
+ DEFAULT_COLOR_MAPPING_CONFIG,
+ DEFAULT_OTHER_ASSIGNMENT_INDEX,
} from '@kbn/coloring';
import faker from 'faker';
-import { DEFAULT_NEUTRAL_PALETTE_INDEX } from '@kbn/coloring/src/shared_components/color_mapping/config/default_color_mapping';
-export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = {
- assignmentMode: 'auto',
- assignments: [],
- specialAssignments: [
- {
- rule: {
- type: 'other',
- },
- color: {
- type: 'categorical',
- paletteId: NeutralPalette.id,
- colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX,
- },
- touched: false,
- },
- ],
- paletteId: EUIAmsterdamColorBlindPalette.id,
- colorMode: {
- type: 'categorical',
- },
-};
-
-const exampleAssignment = (valuesCount = 1, type = 'categorical', overrides = {}) => {
- const color =
+const exampleAssignment = (
+ valuesCount = 1,
+ type = 'categorical',
+ overrides = {}
+): ColorMapping.Config['assignments'][number] => {
+ const color: ColorMapping.Config['assignments'][number]['color'] =
type === 'categorical'
? {
type: 'categorical',
@@ -58,11 +40,10 @@ const exampleAssignment = (valuesCount = 1, type = 'categorical', overrides = {}
color,
touched: false,
...overrides,
- } as ColorMapping.Config['assignments'][0];
+ };
};
const MANUAL_COLOR_MAPPING_CONFIG: ColorMapping.Config = {
- assignmentMode: 'manual',
assignments: [
exampleAssignment(4),
exampleAssignment(),
@@ -90,7 +71,7 @@ const MANUAL_COLOR_MAPPING_CONFIG: ColorMapping.Config = {
const specialAssignmentsPalette: ColorMapping.Config['specialAssignments'] = [
{
- ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0],
+ ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX],
color: {
type: 'categorical',
paletteId: EUIAmsterdamColorBlindPalette.id,
@@ -100,7 +81,7 @@ const specialAssignmentsPalette: ColorMapping.Config['specialAssignments'] = [
];
const specialAssignmentsCustom1: ColorMapping.Config['specialAssignments'] = [
{
- ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0],
+ ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX],
color: {
type: 'colorCode',
colorCode: '#501a0e',
@@ -109,7 +90,7 @@ const specialAssignmentsCustom1: ColorMapping.Config['specialAssignments'] = [
];
const specialAssignmentsCustom2: ColorMapping.Config['specialAssignments'] = [
{
- ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0],
+ ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX],
color: {
type: 'colorCode',
colorCode: 'red',
@@ -129,11 +110,10 @@ describe('color_telemetry_helpers', () => {
getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, MANUAL_COLOR_MAPPING_CONFIG)
).toEqual([]);
});
- it('settings (default): auto color mapping, unassigned terms neutral, default palette returns correct events', () => {
+ it('settings (default): unassigned terms loop, default palette returns correct events', () => {
expect(getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG)).toEqual([
- 'lens_color_mapping_auto',
'lens_color_mapping_palette_eui_amsterdam_color_blind',
- 'lens_color_mapping_unassigned_terms_neutral',
+ 'lens_color_mapping_unassigned_terms_loop',
]);
});
it('gradient event when user changed colorMode to gradient', () => {
@@ -158,9 +138,8 @@ describe('color_telemetry_helpers', () => {
)
).toEqual(['lens_color_mapping_gradient']);
});
- it('settings: manual mode, custom palette, unassigned terms from palette, 2 colors with 5 terms in total', () => {
+ it('settings: custom palette, unassigned terms from palette, 2 colors with 5 terms in total', () => {
expect(getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG)).toEqual([
- 'lens_color_mapping_manual',
'lens_color_mapping_palette_elastic_brand_2023',
'lens_color_mapping_unassigned_terms_palette',
'lens_color_mapping_colors_2_to_4',
@@ -170,7 +149,6 @@ describe('color_telemetry_helpers', () => {
expect(
getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, DEFAULT_COLOR_MAPPING_CONFIG)
).toEqual([
- 'lens_color_mapping_manual',
'lens_color_mapping_palette_elastic_brand_2023',
'lens_color_mapping_unassigned_terms_palette',
'lens_color_mapping_colors_2_to_4',
@@ -254,7 +232,7 @@ describe('color_telemetry_helpers', () => {
});
describe('unassigned terms', () => {
- it('unassigned terms changed from neutral to palette', () => {
+ it('unassigned terms changed from loop to palette', () => {
expect(
getColorMappingTelemetryEvents(
{
@@ -265,15 +243,15 @@ describe('color_telemetry_helpers', () => {
)
).toEqual(['lens_color_mapping_unassigned_terms_palette']);
});
- it('unassigned terms changed from palette to neutral', () => {
+ it('unassigned terms changed from palette to loop', () => {
expect(
getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG, {
...DEFAULT_COLOR_MAPPING_CONFIG,
specialAssignments: specialAssignmentsPalette,
})
- ).toEqual(['lens_color_mapping_unassigned_terms_neutral']);
+ ).toEqual(['lens_color_mapping_unassigned_terms_loop']);
});
- it('unassigned terms changed from neutral to another custom color', () => {
+ it('unassigned terms changed from loop to another custom color', () => {
expect(
getColorMappingTelemetryEvents(
{
diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts
index d6b7acab55c7f..5bbfaaf290ef3 100644
--- a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts
+++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts
@@ -5,12 +5,7 @@
* 2.0.
*/
-import { ColorMapping, NeutralPalette } from '@kbn/coloring';
-import type {
- CategoricalColor,
- ColorCode,
- GradientColor,
-} from '@kbn/coloring/src/shared_components/color_mapping/config/types';
+import { ColorMapping, NeutralPalette, DEFAULT_OTHER_ASSIGNMENT_INDEX } from '@kbn/coloring';
import { isEqual } from 'lodash';
import { nonNullable } from '../utils';
@@ -24,17 +19,14 @@ export const getColorMappingTelemetryEvents = (
return [];
}
- const { assignments, specialAssignments, assignmentMode, colorMode, paletteId } = colorMapping;
+ const { assignments, specialAssignments, colorMode, paletteId } = colorMapping;
const {
- assignmentMode: prevAssignmentMode,
assignments: prevAssignments,
specialAssignments: prevSpecialAssignments,
colorMode: prevColorMode,
paletteId: prevPaletteId,
} = prevColorMapping || {};
- const assignmentModeData = assignmentMode !== prevAssignmentMode ? assignmentMode : undefined;
-
const paletteData = prevPaletteId !== paletteId ? `palette_${paletteId}` : undefined;
const gradientData =
@@ -42,18 +34,16 @@ export const getColorMappingTelemetryEvents = (
const unassignedTermsType = getUnassignedTermsType(specialAssignments, prevSpecialAssignments);
- const diffData = [assignmentModeData, gradientData, paletteData, unassignedTermsType].filter(
- nonNullable
- );
+ const diffData = [gradientData, paletteData, unassignedTermsType].filter(nonNullable);
- if (assignmentMode === 'manual') {
+ if (assignments.length > 0) {
const colorCount =
assignments.length && !isEqual(assignments, prevAssignments)
? `colors_${getRangeText(assignments.length)}`
: undefined;
- const prevCustomColors = prevAssignments?.filter((a) => isCustomColor(a.color));
- const customColors = assignments.filter((a) => isCustomColor(a.color));
+ const prevCustomColors = prevAssignments?.filter((a) => a.color.type === 'colorCode');
+ const customColors = assignments.filter((a) => a.color.type === 'colorCode');
const customColorEvent =
customColors.length && !isEqual(prevCustomColors, customColors)
? `custom_colors_${getRangeText(customColors.length, 1)}`
@@ -68,10 +58,6 @@ export const getColorMappingTelemetryEvents = (
const constructName = (eventName: string) => `${COLOR_MAPPING_PREFIX}${eventName}`;
-const isCustomColor = (color: CategoricalColor | ColorCode | GradientColor): color is ColorCode => {
- return color.type === 'colorCode';
-};
-
function getRangeText(n: number, min = 2, max = 16) {
if (n >= min && (n === 1 || n === 2)) {
return String(n);
@@ -92,9 +78,12 @@ const getUnassignedTermsType = (
) => {
return !isEqual(prevSpecialAssignments, specialAssignments)
? `unassigned_terms_${
- isCustomColor(specialAssignments?.[0].color)
+ specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.type === 'colorCode'
? 'custom'
- : specialAssignments?.[0].color.paletteId === NeutralPalette.id
+ : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.type === 'loop'
+ ? 'loop'
+ : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.paletteId ===
+ NeutralPalette.id
? NeutralPalette.id
: 'palette'
}`
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index b8d9c9eb44c43..a9b62b8622cdc 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -140,8 +140,6 @@
"coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "Cette couleur sera automatiquement affectée au premier terme qui ne correspond pas à toutes les autres affectations",
"coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "Affecté automatiquement",
"coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "Supprimer cette affectation",
- "coloring.colorMapping.assignments.unassignedAriaLabel": "Affecter cette couleur à tout terme non affecté qui n'est pas décrit dans la liste d'affectation",
- "coloring.colorMapping.assignments.unassignedPlaceholder": "Termes non affectés",
"coloring.colorMapping.colorChangesModal.categoricalModeDescription": "Basculer en mode de catégorie conduira à l'abandon de toutes vos modifications de couleurs personnalisées",
"coloring.colorMapping.colorChangesModal.discardButton": "Abandonner les modifications",
"coloring.colorMapping.colorChangesModal.discardButtonLabel": "Abandonner les modifications",
@@ -159,14 +157,11 @@
"coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "Supprimer l'étape couleur",
"coloring.colorMapping.colorPicker.themeAwareColorsLabel": "Couleurs neutres",
"coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "Les couleurs neutres fournies se conforment au thème et s'adapteront en fonction du basculement entre les thèmes clair et sombre",
- "coloring.colorMapping.container.addAssignmentButtonLabel": "Ajouter une affectation",
- "coloring.colorMapping.container.autoAssignLabel": "Affectation automatique",
"coloring.colorMapping.container.invertGradientButtonLabel": "Inverser le gradient",
"coloring.colorMapping.container.mappingAssignmentHeader": "Mapping des affectations",
"coloring.colorMapping.paletteSelector.categoricalLabel": "De catégorie",
"coloring.colorMapping.paletteSelector.paletteLabel": "Palette de couleurs",
"coloring.colorMapping.paletteSelector.scaleLabel": "Scaling",
- "coloring.colorMapping.paletteSelector.sequentialLabel": "Séquentiel",
"coloring.dynamicColoring.customPalette.addColor": "Ajouter une couleur",
"coloring.dynamicColoring.customPalette.addColorAriaLabel": "Ajouter une couleur",
"coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "Les types de valeurs en pourcentage sont relatifs à la plage complète des valeurs de données disponibles.",
@@ -42905,4 +42900,4 @@
"xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet",
"xpack.serverlessObservability.nav.visualizations": "Visualisations"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index cf3ea4c3fe0a9..77335897bd208 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -140,8 +140,6 @@
"coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "この色は、他のすべての割り当てと一致しない最初の用語に自動的に割り当てられます。",
"coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "自動割り当て済み",
"coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "この割り当てを削除",
- "coloring.colorMapping.assignments.unassignedAriaLabel": "割り当てリストに記載されていない未割り当てのすべての色にこの色を割り当てます。",
- "coloring.colorMapping.assignments.unassignedPlaceholder": "割り当てられていない用語",
"coloring.colorMapping.colorChangesModal.categoricalModeDescription": "分類モードに切り替えると、カスタム色の変更はすべて破棄されます。",
"coloring.colorMapping.colorChangesModal.discardButton": "変更を破棄",
"coloring.colorMapping.colorChangesModal.discardButtonLabel": "変更を破棄",
@@ -159,14 +157,11 @@
"coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "色ステップを削除",
"coloring.colorMapping.colorPicker.themeAwareColorsLabel": "中間色",
"coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "提供されている中間色はテーマを意識しており、明るいテーマと暗いテーマを切り替えると適切に変化します。",
- "coloring.colorMapping.container.addAssignmentButtonLabel": "割り当てを追加",
- "coloring.colorMapping.container.autoAssignLabel": "自動割り当て",
"coloring.colorMapping.container.invertGradientButtonLabel": "グラデーションを反転",
"coloring.colorMapping.container.mappingAssignmentHeader": "マッピング割り当て",
"coloring.colorMapping.paletteSelector.categoricalLabel": "分類",
"coloring.colorMapping.paletteSelector.paletteLabel": "カラーパレット",
"coloring.colorMapping.paletteSelector.scaleLabel": "スケール",
- "coloring.colorMapping.paletteSelector.sequentialLabel": "連続",
"coloring.dynamicColoring.customPalette.addColor": "色を追加",
"coloring.dynamicColoring.customPalette.addColorAriaLabel": "色を追加",
"coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "割合値は使用可能なデータ値の全範囲に対して相対的です。",
@@ -42897,4 +42892,4 @@
"xpack.serverlessObservability.nav.projectSettings": "プロジェクト設定",
"xpack.serverlessObservability.nav.visualizations": "ビジュアライゼーション"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 26029615545f8..70fe8f8c91298 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -140,8 +140,6 @@
"coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "会将此颜色自动分配给第一个与所有其他分配均不匹配的词",
"coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "已自动分配",
"coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "删除此分配",
- "coloring.colorMapping.assignments.unassignedAriaLabel": "将此颜色分配给分配列表中未描述的每个未分配项",
- "coloring.colorMapping.assignments.unassignedPlaceholder": "未分配的词",
"coloring.colorMapping.colorChangesModal.categoricalModeDescription": "切换到分类模式将丢弃您的所有定制颜色更改",
"coloring.colorMapping.colorChangesModal.discardButton": "放弃更改",
"coloring.colorMapping.colorChangesModal.discardButtonLabel": "放弃更改",
@@ -159,14 +157,11 @@
"coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "移除色阶",
"coloring.colorMapping.colorPicker.themeAwareColorsLabel": "中性色",
"coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "提供的中性色能够感知主题,在浅色主题与深色主题之间切换时会做出相应更改",
- "coloring.colorMapping.container.addAssignmentButtonLabel": "添加分配",
- "coloring.colorMapping.container.autoAssignLabel": "自动分配",
"coloring.colorMapping.container.invertGradientButtonLabel": "反向渐变",
"coloring.colorMapping.container.mappingAssignmentHeader": "映射分配",
"coloring.colorMapping.paletteSelector.categoricalLabel": "分类",
"coloring.colorMapping.paletteSelector.paletteLabel": "调色板",
"coloring.colorMapping.paletteSelector.scaleLabel": "比例",
- "coloring.colorMapping.paletteSelector.sequentialLabel": "顺序",
"coloring.dynamicColoring.customPalette.addColor": "添加颜色",
"coloring.dynamicColoring.customPalette.addColorAriaLabel": "添加颜色",
"coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "百分比值是相对于全范围可用数据值的类型。",
@@ -42877,4 +42872,4 @@
"xpack.serverlessObservability.nav.projectSettings": "项目设置",
"xpack.serverlessObservability.nav.visualizations": "可视化"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index c159fb17b4db7..c5018bda39ffa 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -1934,8 +1934,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.existOrFail('lns-indexPattern-dimensionContainerClose');
});
await testSubjects.click('lns_colorEditing_trigger');
- // disable autoAssign
- await testSubjects.setEuiSwitch('lns-colorMapping-autoAssignSwitch', 'uncheck');
+
+ // assign all
+ await testSubjects.click('lns-colorMapping-assignmentsPromptAddAll');
await testSubjects.click(`lns-colorMapping-colorSwatch-${colorSwatchIndex}`);
From 69bd69992e1b4f707eeb0ec6cf7892354fe57363 Mon Sep 17 00:00:00 2001
From: jennypavlova
Date: Thu, 8 Feb 2024 14:31:39 +0100
Subject: [PATCH 017/104] [Infra] Kubernetes form - update survey link for
correct cluster version (#176385)
Closes #174347
## Summary
This PR changes the Kibana version query parameter in the k8s feedback
URL to fix the issue with prefilling the form and adds an option to have
more flexibility in the feedback button regarding the form parameter
names.
## Testing
- Open Inventory view and select Kubernetes Pods from the `Show` menu
- Click on the feedback button
- Check if the Kibana version is prefilled correctly in the form
https://github.com/elastic/kibana/assets/14139027/b62edb51-d990-49dc-9214-764dbbf6f47c
---
.../components/survey_kubernetes.tsx | 3 ++
.../feature_feedback_button.tsx | 50 +++++++++++++++----
2 files changed, 43 insertions(+), 10 deletions(-)
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx
index c5524bae4eb41..ce36a396b150f 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx
@@ -35,6 +35,9 @@ export const SurveyKubernetes = () => {
defaultMessage="Tell us what you think! (K8s)"
/>
}
+ formConfig={{
+ kibanaVersionQueryParam: 'entry.184582718',
+ }}
/>
{!isToastSeen && (
{
+const getSurveyFeedbackURL = ({
+ formUrl,
+ formConfig,
+ kibanaVersion,
+ deploymentType,
+ sanitizedPath,
+}: {
+ formUrl: string;
+ formConfig?: FormConfig;
+ kibanaVersion?: string;
+ deploymentType?: string;
+ sanitizedPath?: string;
+}) => {
const url = new URL(formUrl);
if (kibanaVersion) {
- url.searchParams.append(KIBANA_VERSION_QUERY_PARAM, kibanaVersion);
+ url.searchParams.append(
+ formConfig?.kibanaVersionQueryParam || KIBANA_VERSION_QUERY_PARAM,
+ kibanaVersion
+ );
}
if (deploymentType) {
- url.searchParams.append(KIBANA_DEPLOYMENT_TYPE_PARAM, deploymentType);
+ url.searchParams.append(
+ formConfig?.kibanaDeploymentTypeQueryParam || KIBANA_DEPLOYMENT_TYPE_PARAM,
+ deploymentType
+ );
}
if (sanitizedPath) {
- url.searchParams.append(SANITIZED_PATH_PARAM, sanitizedPath);
+ url.searchParams.append(
+ formConfig?.sanitizedPathQueryParam || SANITIZED_PATH_PARAM,
+ sanitizedPath
+ );
}
return url.href;
};
+interface FormConfig {
+ kibanaVersionQueryParam?: string;
+ kibanaDeploymentTypeQueryParam?: string;
+ sanitizedPathQueryParam?: string;
+}
+
interface FeatureFeedbackButtonProps {
formUrl: string;
'data-test-subj': string;
@@ -53,10 +75,12 @@ interface FeatureFeedbackButtonProps {
isCloudEnv?: boolean;
isServerlessEnv?: boolean;
sanitizedPath?: string;
+ formConfig?: FormConfig;
}
export const FeatureFeedbackButton = ({
formUrl,
+ formConfig,
'data-test-subj': dts,
onClickCapture,
defaultButton,
@@ -78,7 +102,13 @@ export const FeatureFeedbackButton = ({
return (
Date: Thu, 8 Feb 2024 15:12:30 +0100
Subject: [PATCH 018/104] More ES|QL in-product help updates for 8.13 (#176330)
---
.../src/esql_documentation_sections.tsx | 245 +++++++++++++++---
1 file changed, 216 insertions(+), 29 deletions(-)
diff --git a/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx
index 6f96c1366800d..2c47f7f309531 100644
--- a/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx
+++ b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx
@@ -585,9 +585,12 @@ FROM employees
defaultMessage: `### STATS ... BY
Use \`STATS ... BY\` to group rows according to a common value and calculate one or more aggregated values over the grouped rows.
+**Examples**:
+
\`\`\`
FROM employees
-| STATS count = COUNT(languages) BY languages
+| STATS count = COUNT(emp_no) BY languages
+| SORT languages
\`\`\`
If \`BY\` is omitted, the output table contains exactly one row with the aggregations applied over the entire dataset:
@@ -615,6 +618,40 @@ FROM employees
\`\`\`
Refer to **Aggregation functions** for a list of functions that can be used with \`STATS ... BY\`.
+
+Both the aggregating functions and the grouping expressions accept other functions. This is useful for using \`STATS...BY\` on multivalue columns. For example, to calculate the average salary change, you can use \`MV_AVG\` to first average the multiple values per employee, and use the result with the \`AVG\` function:
+
+\`\`\`
+FROM employees
+| STATS avg_salary_change = AVG(MV_AVG(salary_change))
+\`\`\`
+
+An example of grouping by an expression is grouping employees on the first letter of their last name:
+
+\`\`\`
+FROM employees
+| STATS my_count = COUNT() BY LEFT(last_name, 1)
+| SORT \`LEFT(last_name, 1)\`
+\`\`\`
+
+Specifying the output column name is optional. If not specified, the new column name is equal to the expression. The following query returns a column named \`AVG(salary)\`:
+
+\`\`\`
+FROM employees
+| STATS AVG(salary)
+\`\`\`
+
+Because this name contains special characters, it needs to be quoted with backticks (\`) when using it in subsequent commands:
+
+\`\`\`
+FROM employees
+| STATS AVG(salary)
+| EVAL avg_salary_rounded = ROUND(\`AVG(salary)\`)
+\`\`\`
+
+**Note**: \`STATS\` without any groups is much faster than adding a group.
+
+**Note**: Grouping on a single expression is currently much more optimized than grouping on many expressions.
`,
description:
'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
@@ -934,7 +971,7 @@ ROW a=1.8
| EVAL a=CEIL(a)
\`\`\`
-Note: This is a noop for \`long\` (including unsigned) and \`integer\`. For \`double\` this picks the the closest \`double\` value to the integer similar to Java's \`Math.ceil\`.
+Note: This is a noop for \`long\` (including unsigned) and \`integer\`. For \`double\` this picks the closest \`double\` value to the integer similar to Java's \`Math.ceil\`.
`,
description:
'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
@@ -1081,6 +1118,33 @@ ROW a=1.8
/>
),
},
+ {
+ label: i18n.translate(
+ 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dateDiffFunction',
+ {
+ defaultMessage: 'DATE_DIFF',
+ }
+ ),
+ description: (
+
+ ),
+ },
{
label: i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dateExtractFunction',
@@ -1099,6 +1163,13 @@ Extracts parts of a date, like year, month, day, hour. The supported field types
\`\`\`
ROW date = DATE_PARSE("yyyy-MM-dd", "2022-05-06")
| EVAL year = DATE_EXTRACT("year", date)
+\`\`\`
+
+For example, to find all events that occurred outside of business hours (before 9 AM or after 5 PM), on any given date:
+
+\`\`\`
+FROM sample_data
+| WHERE DATE_EXTRACT("hour_of_day", @timestamp) < 9 AND DATE_EXTRACT("hour_of_day", @timestamp) >= 17
\`\`\`
`,
description:
@@ -1145,12 +1216,13 @@ FROM employees
),
description: (
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stCentroidFunction',
+ {
+ defaultMessage: 'ST_CENTROID',
+ }
+ ),
+ description: (
+
Date: Thu, 8 Feb 2024 08:32:11 -0600
Subject: [PATCH 019/104] [Console] Add support to remotely open Embeddable
Console (#176017)
## Summary
Updated the embeddable console and console start to allow triggering the
Console open from the ConsolePluginStart object with or without specific
content to be pre-loaded.
### Checklist
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/code_box.tsx | 4 +
.../components/ingest_data.tsx | 4 +
.../components/try_in_console_button.tsx | 17 +-
packages/kbn-search-api-panels/tsconfig.json | 3 +-
.../editor/legacy/console_editor/editor.tsx | 2 +-
.../containers/embeddable/console_wrapper.tsx | 16 +-
.../embeddable/embeddable_console.tsx | 30 ++-
.../public/application/lib/load_from.test.ts | 236 ++++++++++++++++++
.../public/application/lib/load_from.ts | 49 ++++
.../application/stores/embeddable_console.ts | 41 +++
src/plugins/console/public/plugin.ts | 12 +-
.../services/embeddable_console.test.ts | 53 ++++
.../public/services/embeddable_console.ts | 29 +++
src/plugins/console/public/services/index.ts | 1 +
.../public/types/embeddable_console.ts | 11 +
.../public/types/plugin_dependencies.ts | 10 +
.../common/types/kibana_deps.ts | 2 +
.../panels/add_data_panel_content.tsx | 1 +
.../panels/search_query_panel_content.tsx | 1 +
.../application/components/overview.tsx | 2 +
20 files changed, 510 insertions(+), 14 deletions(-)
create mode 100644 src/plugins/console/public/application/lib/load_from.test.ts
create mode 100644 src/plugins/console/public/application/lib/load_from.ts
create mode 100644 src/plugins/console/public/application/stores/embeddable_console.ts
create mode 100644 src/plugins/console/public/services/embeddable_console.test.ts
create mode 100644 src/plugins/console/public/services/embeddable_console.ts
diff --git a/packages/kbn-search-api-panels/components/code_box.tsx b/packages/kbn-search-api-panels/components/code_box.tsx
index 21c4085f44a9b..5a4ff7cc13240 100644
--- a/packages/kbn-search-api-panels/components/code_box.tsx
+++ b/packages/kbn-search-api-panels/components/code_box.tsx
@@ -23,6 +23,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { ApplicationStart } from '@kbn/core-application-browser';
+import type { ConsolePluginStart } from '@kbn/console-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { LanguageDefinition } from '../types';
@@ -38,6 +39,7 @@ interface CodeBoxProps {
setSelectedLanguage: (language: LanguageDefinition) => void;
assetBasePath: string;
application?: ApplicationStart;
+ consolePlugin?: ConsolePluginStart;
sharePlugin: SharePluginStart;
consoleRequest?: string;
}
@@ -45,6 +47,7 @@ interface CodeBoxProps {
export const CodeBox: React.FC = ({
application,
codeSnippet,
+ consolePlugin,
languageType,
languages,
assetBasePath,
@@ -117,6 +120,7 @@ export const CodeBox: React.FC = ({
diff --git a/packages/kbn-search-api-panels/components/ingest_data.tsx b/packages/kbn-search-api-panels/components/ingest_data.tsx
index f8ba59d29bf30..0700d2d56d661 100644
--- a/packages/kbn-search-api-panels/components/ingest_data.tsx
+++ b/packages/kbn-search-api-panels/components/ingest_data.tsx
@@ -11,6 +11,7 @@ import React from 'react';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { ApplicationStart } from '@kbn/core-application-browser';
+import type { ConsolePluginStart } from '@kbn/console-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { CodeBox } from './code_box';
import { LanguageDefinition } from '../types';
@@ -26,6 +27,7 @@ interface IngestDataProps {
};
assetBasePath: string;
application?: ApplicationStart;
+ consolePlugin?: ConsolePluginStart;
sharePlugin: SharePluginStart;
languages: LanguageDefinition[];
consoleRequest?: string;
@@ -39,6 +41,7 @@ export const IngestData: React.FC = ({
docLinks,
assetBasePath,
application,
+ consolePlugin,
sharePlugin,
languages,
consoleRequest,
@@ -58,6 +61,7 @@ export const IngestData: React.FC = ({
setSelectedLanguage={setSelectedLanguage}
assetBasePath={assetBasePath}
application={application}
+ consolePlugin={consolePlugin}
sharePlugin={sharePlugin}
/>
}
diff --git a/packages/kbn-search-api-panels/components/try_in_console_button.tsx b/packages/kbn-search-api-panels/components/try_in_console_button.tsx
index fe109e025e2e5..7007c306a2cd6 100644
--- a/packages/kbn-search-api-panels/components/try_in_console_button.tsx
+++ b/packages/kbn-search-api-panels/components/try_in_console_button.tsx
@@ -11,6 +11,7 @@ import React from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import type { ApplicationStart } from '@kbn/core-application-browser';
import type { SharePluginStart } from '@kbn/share-plugin/public';
+import type { ConsolePluginStart } from '@kbn/console-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { compressToEncodedURIComponent } from 'lz-string';
@@ -18,11 +19,13 @@ import { compressToEncodedURIComponent } from 'lz-string';
export interface TryInConsoleButtonProps {
request: string;
application?: ApplicationStart;
+ consolePlugin?: ConsolePluginStart;
sharePlugin: SharePluginStart;
}
export const TryInConsoleButton = ({
request,
application,
+ consolePlugin,
sharePlugin,
}: TryInConsoleButtonProps) => {
const { url } = sharePlugin;
@@ -39,8 +42,20 @@ export const TryInConsoleButton = ({
);
if (!consolePreviewLink) return null;
+ const onClick = () => {
+ const embeddedConsoleAvailable =
+ (consolePlugin?.openEmbeddedConsole !== undefined &&
+ consolePlugin?.isEmbeddedConsoleAvailable?.()) ??
+ false;
+ if (embeddedConsoleAvailable) {
+ consolePlugin!.openEmbeddedConsole!(request);
+ } else {
+ window.open(consolePreviewLink, '_blank', 'noreferrer');
+ }
+ };
+
return (
-
+
{
- const [, queryString] = (window.location.hash || '').split('?');
+ const [, queryString] = (window.location.hash || window.location.search || '').split('?');
return parse(queryString || '', { sort: false }) as Required;
};
diff --git a/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx b/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx
index 340b20c91b6cd..3618194e194dc 100644
--- a/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx
+++ b/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx
@@ -14,8 +14,10 @@ import {
I18nStart,
CoreTheme,
DocLinksStart,
+ CoreStart,
} from '@kbn/core/public';
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
+import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import { ObjectStorageClient } from '../../../../common/types';
@@ -62,10 +64,10 @@ interface ConsoleDependencies {
trackUiMetric: MetricsTracker;
}
-const loadDependencies = async ({
- core,
- usageCollection,
-}: EmbeddableConsoleDependencies): Promise => {
+const loadDependencies = async (
+ core: CoreStart,
+ usageCollection?: UsageCollectionStart
+): Promise => {
const {
docLinks: { DOC_LINK_VERSION, links },
http,
@@ -107,10 +109,12 @@ const loadDependencies = async ({
};
};
-export const ConsoleWrapper = (props: EmbeddableConsoleDependencies): React.ReactElement => {
+type ConsoleWrapperProps = Omit;
+
+export const ConsoleWrapper: React.FunctionComponent = (props) => {
const [dependencies, setDependencies] = useState(null);
useEffect(() => {
- loadDependencies(props).then(setDependencies);
+ loadDependencies(props.core, props.usageCollection).then(setDependencies);
}, [setDependencies, props]);
useEffect(() => {
return () => {
diff --git a/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx b/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx
index 2577c9d4841d7..2167ec12c52c0 100644
--- a/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx
+++ b/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import React, { useState } from 'react';
+import React, { useReducer, useEffect } from 'react';
import classNames from 'classnames';
import useObservable from 'react-use/lib/useObservable';
import {
@@ -25,6 +25,9 @@ import {
EmbeddableConsoleDependencies,
} from '../../../types/embeddable_console';
+import * as store from '../../stores/embeddable_console';
+import { setLoadFromParameter, removeLoadFromParameter } from '../../lib/load_from';
+
import { ConsoleWrapper } from './console_wrapper';
import './_index.scss';
@@ -36,10 +39,31 @@ export const EmbeddableConsole = ({
size = 'm',
core,
usageCollection,
+ setDispatch,
}: EmbeddableConsoleProps & EmbeddableConsoleDependencies) => {
- const [isConsoleOpen, setIsConsoleOpen] = useState(false);
- const toggleConsole = () => setIsConsoleOpen(!isConsoleOpen);
+ const [consoleState, consoleDispatch] = useReducer(
+ store.reducer,
+ store.initialValue,
+ (value) => ({ ...value })
+ );
const chromeStyle = useObservable(core.chrome.getChromeStyle$());
+ useEffect(() => {
+ setDispatch(consoleDispatch);
+ return () => setDispatch(null);
+ }, [setDispatch, consoleDispatch]);
+ useEffect(() => {
+ if (consoleState.isOpen && consoleState.loadFromContent) {
+ setLoadFromParameter(consoleState.loadFromContent);
+ } else if (!consoleState.isOpen) {
+ removeLoadFromParameter();
+ }
+ }, [consoleState.isOpen, consoleState.loadFromContent]);
+
+ const isConsoleOpen = consoleState.isOpen;
+ const setIsConsoleOpen = (value: boolean) => {
+ consoleDispatch(value ? { type: 'open' } : { type: 'close' });
+ };
+ const toggleConsole = () => setIsConsoleOpen(!isConsoleOpen);
const onKeyDown = (event: any) => {
if (event.key === keys.ESCAPE) {
diff --git a/src/plugins/console/public/application/lib/load_from.test.ts b/src/plugins/console/public/application/lib/load_from.test.ts
new file mode 100644
index 0000000000000..289718f46cf10
--- /dev/null
+++ b/src/plugins/console/public/application/lib/load_from.test.ts
@@ -0,0 +1,236 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { compressToEncodedURIComponent } from 'lz-string';
+import { setLoadFromParameter, removeLoadFromParameter } from './load_from';
+
+const baseMockWindow = () => {
+ return {
+ history: {
+ pushState: jest.fn(),
+ },
+ location: {
+ host: 'my-kibana.elastic.co',
+ pathname: '',
+ protocol: 'https:',
+ search: '',
+ hash: '',
+ },
+ };
+};
+let windowSpy: jest.SpyInstance;
+let mockWindow = baseMockWindow();
+
+describe('load from lib', () => {
+ beforeEach(() => {
+ mockWindow = baseMockWindow();
+ windowSpy = jest.spyOn(globalThis, 'window', 'get');
+ windowSpy.mockImplementation(() => mockWindow);
+ });
+
+ afterEach(() => {
+ windowSpy.mockRestore();
+ });
+
+ describe('setLoadFromParameter', () => {
+ it('adds load_from as expected', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ hash: '#/console',
+ };
+ const codeSnippet = 'GET /_stats';
+ const expectedUrl =
+ 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA';
+
+ setLoadFromParameter(codeSnippet);
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('can replace an existing value', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ hash: `#/console?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgxgQwC5QJYDsAmq4FMDOQA`,
+ };
+ const codeSnippet = 'GET /_stats';
+ const expectedUrl =
+ 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA';
+
+ setLoadFromParameter(codeSnippet);
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('works with other query params', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ hash: '#/console?foo=bar',
+ };
+ const codeSnippet = 'GET /_stats';
+ const expectedUrl =
+ 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?foo=bar&load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA';
+
+ setLoadFromParameter(codeSnippet);
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('works with a non-hash route', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/enterprise_search/overview',
+ };
+ const codeSnippet = 'GET /_stats';
+ const expectedUrl =
+ 'https://my-kibana.elastic.co/foo/app/enterprise_search/overview?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA';
+
+ setLoadFromParameter(codeSnippet);
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('works with a non-hash route and other params', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/enterprise_search/overview',
+ search: '?foo=bar',
+ };
+ const codeSnippet = 'GET /_stats';
+ const expectedUrl =
+ 'https://my-kibana.elastic.co/foo/app/enterprise_search/overview?foo=bar&load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA';
+
+ setLoadFromParameter(codeSnippet);
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ });
+
+ describe('removeLoadFromParameter', () => {
+ it('leaves other params in place', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ search: `?foo=bar&load_from=data:text/plain,${compressToEncodedURIComponent(
+ 'GET /_cat/indices'
+ )}`,
+ };
+
+ const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools?foo=bar';
+
+ removeLoadFromParameter();
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('leaves other params with a hashroute', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ hash: `#/console?foo=bar&load_from=data:text/plain,${compressToEncodedURIComponent(
+ 'GET /_cat/indices'
+ )}`,
+ };
+
+ const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?foo=bar';
+
+ removeLoadFromParameter();
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('removes ? if load_from was the only param', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ search: `?load_from=data:text/plain,${compressToEncodedURIComponent('GET /_cat/indices')}`,
+ };
+
+ const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools';
+
+ removeLoadFromParameter();
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('removes ? if load_from was the only param in a hashroute', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ hash: `#/console?load_from=data:text/plain,${compressToEncodedURIComponent(
+ 'GET /_cat/indices'
+ )}`,
+ };
+
+ const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools#/console';
+
+ removeLoadFromParameter();
+ expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
+ expect(mockWindow.history.pushState).toHaveBeenCalledWith(
+ {
+ path: expectedUrl,
+ },
+ '',
+ expectedUrl
+ );
+ });
+ it('noop if load_from not currently defined on QS', () => {
+ mockWindow.location = {
+ ...mockWindow.location,
+ pathname: '/foo/app/dev_tools',
+ hash: `#/console?foo=bar`,
+ };
+
+ removeLoadFromParameter();
+ expect(mockWindow.history.pushState).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/plugins/console/public/application/lib/load_from.ts b/src/plugins/console/public/application/lib/load_from.ts
new file mode 100644
index 0000000000000..c601eafb6f3a9
--- /dev/null
+++ b/src/plugins/console/public/application/lib/load_from.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import qs from 'query-string';
+import { compressToEncodedURIComponent } from 'lz-string';
+
+function getBaseUrl() {
+ return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
+}
+function parseQueryString() {
+ const [hashRoute, queryString] = (window.location.hash || window.location.search || '').split(
+ '?'
+ );
+
+ const parsedQueryString = qs.parse(queryString || '', { sort: false });
+ return {
+ hasHash: !!window.location.hash,
+ hashRoute,
+ queryString: parsedQueryString,
+ };
+}
+
+export const setLoadFromParameter = (value: string) => {
+ const baseUrl = getBaseUrl();
+ const { hasHash, hashRoute, queryString } = parseQueryString();
+ const consoleDataUri = compressToEncodedURIComponent(value);
+ queryString.load_from = `data:text/plain,${consoleDataUri}`;
+ const params = `?${qs.stringify(queryString)}`;
+ const newUrl = hasHash ? `${baseUrl}${hashRoute}${params}` : `${baseUrl}${params}`;
+
+ window.history.pushState({ path: newUrl }, '', newUrl);
+};
+
+export const removeLoadFromParameter = () => {
+ const baseUrl = getBaseUrl();
+ const { hasHash, hashRoute, queryString } = parseQueryString();
+ if (queryString.load_from) {
+ delete queryString.load_from;
+
+ const params = Object.keys(queryString).length ? `?${qs.stringify(queryString)}` : '';
+ const newUrl = hasHash ? `${baseUrl}${hashRoute}${params}` : `${baseUrl}${params}`;
+ window.history.pushState({ path: newUrl }, '', newUrl);
+ }
+};
diff --git a/src/plugins/console/public/application/stores/embeddable_console.ts b/src/plugins/console/public/application/stores/embeddable_console.ts
new file mode 100644
index 0000000000000..4bfb38d094c13
--- /dev/null
+++ b/src/plugins/console/public/application/stores/embeddable_console.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Reducer } from 'react';
+import { produce } from 'immer';
+import { identity } from 'fp-ts/lib/function';
+
+import { EmbeddedConsoleAction, EmbeddedConsoleStore } from '../../types/embeddable_console';
+
+export const initialValue: EmbeddedConsoleStore = produce(
+ {
+ isOpen: false,
+ },
+ identity
+);
+
+export const reducer: Reducer = (state, action) =>
+ produce(state, (draft) => {
+ switch (action.type) {
+ case 'open':
+ if (!state.isOpen) {
+ draft.isOpen = true;
+ draft.loadFromContent = action.payload?.content;
+ return;
+ }
+ break;
+ case 'close':
+ if (state.isOpen) {
+ draft.isOpen = false;
+ draft.loadFromContent = undefined;
+ return;
+ }
+ break;
+ }
+ return draft;
+ });
diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts
index 711b4304af9b0..53cb16befe865 100644
--- a/src/plugins/console/public/plugin.ts
+++ b/src/plugins/console/public/plugin.ts
@@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
import { i18n } from '@kbn/i18n';
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/public';
@@ -20,10 +19,12 @@ import {
EmbeddableConsoleProps,
EmbeddableConsoleDependencies,
} from './types';
-import { AutocompleteInfo, setAutocompleteInfo } from './services';
+import { AutocompleteInfo, setAutocompleteInfo, EmbeddableConsoleInfo } from './services';
export class ConsoleUIPlugin implements Plugin {
private readonly autocompleteInfo = new AutocompleteInfo();
+ private _embeddableConsole: EmbeddableConsoleInfo = new EmbeddableConsoleInfo();
+
constructor(private ctx: PluginInitializerContext) {}
public setup(
@@ -118,9 +119,16 @@ export class ConsoleUIPlugin implements Plugin {
+ this._embeddableConsole.setDispatch(d);
+ },
};
return renderEmbeddableConsole(props, consoleDeps);
};
+ consoleStart.isEmbeddedConsoleAvailable = () =>
+ this._embeddableConsole.isEmbeddedConsoleAvailable();
+ consoleStart.openEmbeddedConsole = (content?: string) =>
+ this._embeddableConsole.openEmbeddedConsole(content);
}
return consoleStart;
diff --git a/src/plugins/console/public/services/embeddable_console.test.ts b/src/plugins/console/public/services/embeddable_console.test.ts
new file mode 100644
index 0000000000000..7df8230b6dbdf
--- /dev/null
+++ b/src/plugins/console/public/services/embeddable_console.test.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EmbeddableConsoleInfo } from './embeddable_console';
+
+describe('EmbeddableConsoleInfo', () => {
+ let eConsole: EmbeddableConsoleInfo;
+ beforeEach(() => {
+ eConsole = new EmbeddableConsoleInfo();
+ });
+ describe('isEmbeddedConsoleAvailable', () => {
+ it('returns true if dispatch has been set', () => {
+ eConsole.setDispatch(jest.fn());
+ expect(eConsole.isEmbeddedConsoleAvailable()).toBe(true);
+ });
+ it('returns false if dispatch has not been set', () => {
+ expect(eConsole.isEmbeddedConsoleAvailable()).toBe(false);
+ });
+ it('returns false if dispatch has been cleared', () => {
+ eConsole.setDispatch(jest.fn());
+ eConsole.setDispatch(null);
+ expect(eConsole.isEmbeddedConsoleAvailable()).toBe(false);
+ });
+ });
+ describe('openEmbeddedConsole', () => {
+ const mockDispatch = jest.fn();
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ eConsole.setDispatch(mockDispatch);
+ });
+ it('dispatches open action', () => {
+ eConsole.openEmbeddedConsole();
+
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith({ type: 'open' });
+ });
+ it('can set content', () => {
+ eConsole.openEmbeddedConsole('GET /_cat/_indices');
+
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: 'open',
+ payload: { content: 'GET /_cat/_indices' },
+ });
+ });
+ });
+});
diff --git a/src/plugins/console/public/services/embeddable_console.ts b/src/plugins/console/public/services/embeddable_console.ts
new file mode 100644
index 0000000000000..6bc32b8475eef
--- /dev/null
+++ b/src/plugins/console/public/services/embeddable_console.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import type { Dispatch } from 'react';
+
+import { EmbeddedConsoleAction as EmbeddableConsoleAction } from '../types/embeddable_console';
+
+export class EmbeddableConsoleInfo {
+ private _dispatch: Dispatch | null = null;
+
+ public setDispatch(d: Dispatch | null) {
+ this._dispatch = d;
+ }
+
+ public isEmbeddedConsoleAvailable(): boolean {
+ return this._dispatch !== null;
+ }
+
+ public openEmbeddedConsole(content?: string) {
+ // Embedded Console is not rendered on the page, nothing to do
+ if (!this._dispatch) return;
+
+ this._dispatch({ type: 'open', payload: content ? { content } : undefined });
+ }
+}
diff --git a/src/plugins/console/public/services/index.ts b/src/plugins/console/public/services/index.ts
index 73929f89e386f..669ed890729dc 100644
--- a/src/plugins/console/public/services/index.ts
+++ b/src/plugins/console/public/services/index.ts
@@ -16,3 +16,4 @@ export {
setAutocompleteInfo,
ENTITIES,
} from './autocomplete';
+export { EmbeddableConsoleInfo } from './embeddable_console';
diff --git a/src/plugins/console/public/types/embeddable_console.ts b/src/plugins/console/public/types/embeddable_console.ts
index da0e3346a7bd2..71a755b7dd544 100644
--- a/src/plugins/console/public/types/embeddable_console.ts
+++ b/src/plugins/console/public/types/embeddable_console.ts
@@ -7,6 +7,7 @@
*/
import type { CoreStart } from '@kbn/core/public';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
+import type { Dispatch } from 'react';
/**
* EmbeddableConsoleProps are optional props used when rendering the embeddable developer console.
@@ -21,4 +22,14 @@ export interface EmbeddableConsoleProps {
export interface EmbeddableConsoleDependencies {
core: CoreStart;
usageCollection?: UsageCollectionStart;
+ setDispatch: (dispatch: Dispatch | null) => void;
+}
+
+export type EmbeddedConsoleAction =
+ | { type: 'open'; payload?: { content?: string } }
+ | { type: 'close' };
+
+export interface EmbeddedConsoleStore {
+ isOpen: boolean;
+ loadFromContent?: string;
}
diff --git a/src/plugins/console/public/types/plugin_dependencies.ts b/src/plugins/console/public/types/plugin_dependencies.ts
index e4f65d44cb727..199e59d4b9b92 100644
--- a/src/plugins/console/public/types/plugin_dependencies.ts
+++ b/src/plugins/console/public/types/plugin_dependencies.ts
@@ -47,4 +47,14 @@ export interface ConsolePluginStart {
* render an embeddable version of the developer console on the page.
*/
renderEmbeddableConsole?: (props?: EmbeddableConsoleProps) => ReactElement | null;
+ /**
+ * isEmbeddedConsoleAvailable is available if the embedded console can be rendered. Returns true when
+ * called if the Embedded Console is currently rendered.
+ */
+ isEmbeddedConsoleAvailable?: () => boolean;
+ /**
+ * openEmbeddedConsole is available if the embedded console can be rendered. Calling
+ * this function will open the embedded console on the page if it is currently rendered.
+ */
+ openEmbeddedConsole?: (content?: string) => void;
}
diff --git a/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts b/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts
index 2379692abb736..4ab0cfae0932d 100644
--- a/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts
+++ b/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts
@@ -7,6 +7,7 @@
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { CloudStart } from '@kbn/cloud-plugin/public';
+import type { ConsolePluginStart } from '@kbn/console-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { FeaturesPluginStart } from '@kbn/features-plugin/public';
@@ -19,6 +20,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
export interface KibanaDeps {
charts: ChartsPluginStart;
cloud: CloudStart;
+ console?: ConsolePluginStart;
data: DataPublicPluginStart;
discover: DiscoverStart;
features: FeaturesPluginStart;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx
index e834e9ff45fe5..5d2d8cecf8466 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx
@@ -43,6 +43,7 @@ export const AddDataPanelContent: React.FC = ({
setSelectedLanguage={setSelectedLanguage}
assetBasePath={assetBasePath}
application={services.application}
+ consolePlugin={services.console}
sharePlugin={services.share}
/>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx
index 2ce2801f033e0..d32614865b614 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx
@@ -47,6 +47,7 @@ export const SearchQueryPanelContent: React.FC = (
setSelectedLanguage={setSelectedLanguage}
assetBasePath={assetBasePath}
application={services.application}
+ consolePlugin={services.console}
sharePlugin={services.share}
/>
);
diff --git a/x-pack/plugins/serverless_search/public/application/components/overview.tsx b/x-pack/plugins/serverless_search/public/application/components/overview.tsx
index 2a2313564c2e2..1cb337e6584b9 100644
--- a/x-pack/plugins/serverless_search/public/application/components/overview.tsx
+++ b/x-pack/plugins/serverless_search/public/application/components/overview.tsx
@@ -250,6 +250,7 @@ export const ElasticsearchOverview = () => {
assetBasePath={assetBasePath}
docLinks={docLinks}
application={application}
+ consolePlugin={consolePlugin}
sharePlugin={share}
additionalIngestionPanel={ }
/>
@@ -277,6 +278,7 @@ export const ElasticsearchOverview = () => {
setSelectedLanguage={setSelectedLanguage}
assetBasePath={assetBasePath}
application={application}
+ consolePlugin={consolePlugin}
sharePlugin={share}
/>
}
From d8f44220c00ed51ab86ee5f201ff70f2235b1214 Mon Sep 17 00:00:00 2001
From: Julia Rechkunova
Date: Thu, 8 Feb 2024 15:37:50 +0100
Subject: [PATCH 020/104] [Discover] Include global filters when opening a
saved search (#175814)
- Closes https://github.com/elastic/kibana/issues/171212
## Summary
Discover ignored global filters when loading a saved search, because it
loads a saved search before it starts syncing with global URL state.
This PR does not change the order of events but it adds a manual sync
for global filters so they are included in search request anyway.
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../discover_saved_search_container.ts | 31 +++++-
.../main/services/discover_state.ts | 1 +
.../main/services/load_saved_search.ts | 25 ++++-
.../apps/discover/group1/_url_state.ts | 102 ++++++++++++++++++
4 files changed, 155 insertions(+), 4 deletions(-)
diff --git a/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts b/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts
index ef88aba74d7db..ecae2208bffcc 100644
--- a/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts
+++ b/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts
@@ -8,6 +8,7 @@
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { BehaviorSubject } from 'rxjs';
+import { cloneDeep } from 'lodash';
import { COMPARE_ALL_OPTIONS, FilterCompareOptions } from '@kbn/es-query';
import type { SearchSourceFields } from '@kbn/data-plugin/common';
import type { DataView } from '@kbn/data-views-plugin/common';
@@ -110,6 +111,11 @@ export interface DiscoverSavedSearchContainer {
* @param params
*/
update: (params: UpdateParams) => SavedSearch;
+ /**
+ * Passes filter manager filters to saved search filters
+ * @param params
+ */
+ updateWithFilterManagerFilters: () => SavedSearch;
}
export function getSavedSearchContainer({
@@ -169,6 +175,26 @@ export function getSavedSearchContainer({
}
return { id };
};
+
+ const assignNextSavedSearch = ({ nextSavedSearch }: { nextSavedSearch: SavedSearch }) => {
+ const hasChanged = !isEqualSavedSearch(savedSearchInitial$.getValue(), nextSavedSearch);
+ hasChanged$.next(hasChanged);
+ savedSearchCurrent$.next(nextSavedSearch);
+ };
+
+ const updateWithFilterManagerFilters = () => {
+ const nextSavedSearch: SavedSearch = {
+ ...getState(),
+ };
+
+ nextSavedSearch.searchSource.setField('filter', cloneDeep(services.filterManager.getFilters()));
+
+ assignNextSavedSearch({ nextSavedSearch });
+
+ addLog('[savedSearch] updateWithFilterManagerFilters done', nextSavedSearch);
+ return nextSavedSearch;
+ };
+
const update = ({ nextDataView, nextState, useFilterAndQueryServices }: UpdateParams) => {
addLog('[savedSearch] update', { nextDataView, nextState });
@@ -186,9 +212,7 @@ export function getSavedSearchContainer({
useFilterAndQueryServices,
});
- const hasChanged = !isEqualSavedSearch(savedSearchInitial$.getValue(), nextSavedSearch);
- hasChanged$.next(hasChanged);
- savedSearchCurrent$.next(nextSavedSearch);
+ assignNextSavedSearch({ nextSavedSearch });
addLog('[savedSearch] update done', nextSavedSearch);
return nextSavedSearch;
@@ -221,6 +245,7 @@ export function getSavedSearchContainer({
persist,
set,
update,
+ updateWithFilterManagerFilters,
};
}
diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts
index af3675156a93d..79577a8f8e616 100644
--- a/src/plugins/discover/public/application/main/services/discover_state.ts
+++ b/src/plugins/discover/public/application/main/services/discover_state.ts
@@ -357,6 +357,7 @@ export function getDiscoverStateContainer({
dataStateContainer,
internalStateContainer,
savedSearchContainer,
+ globalStateContainer,
services,
setDataView,
});
diff --git a/src/plugins/discover/public/application/main/services/load_saved_search.ts b/src/plugins/discover/public/application/main/services/load_saved_search.ts
index a921e7a69e58c..30ed1792a50e0 100644
--- a/src/plugins/discover/public/application/main/services/load_saved_search.ts
+++ b/src/plugins/discover/public/application/main/services/load_saved_search.ts
@@ -22,6 +22,7 @@ import {
DiscoverAppStateContainer,
getInitialState,
} from './discover_app_state_container';
+import { DiscoverGlobalStateContainer } from './discover_global_state_container';
import { DiscoverServices } from '../../../build_services';
interface LoadSavedSearchDeps {
@@ -29,6 +30,7 @@ interface LoadSavedSearchDeps {
dataStateContainer: DiscoverDataStateContainer;
internalStateContainer: DiscoverInternalStateContainer;
savedSearchContainer: DiscoverSavedSearchContainer;
+ globalStateContainer: DiscoverGlobalStateContainer;
services: DiscoverServices;
setDataView: DiscoverStateContainer['actions']['setDataView'];
}
@@ -44,7 +46,13 @@ export const loadSavedSearch = async (
): Promise => {
addLog('[discoverState] loadSavedSearch');
const { savedSearchId } = params ?? {};
- const { appStateContainer, internalStateContainer, savedSearchContainer, services } = deps;
+ const {
+ appStateContainer,
+ internalStateContainer,
+ savedSearchContainer,
+ globalStateContainer,
+ services,
+ } = deps;
const appStateExists = !appStateContainer.isEmptyURL();
const appState = appStateExists ? appStateContainer.getState() : undefined;
@@ -59,6 +67,15 @@ export const loadSavedSearch = async (
services.filterManager.setAppFilters([]);
services.data.query.queryString.clearQuery();
+ // Sync global filters (coming from URL) to filter manager.
+ // It needs to be done manually here as `syncGlobalQueryStateWithUrl` is being called after this `loadSavedSearch` function.
+ const globalFilters = globalStateContainer?.get()?.filters;
+ const shouldUpdateWithGlobalFilters =
+ globalFilters?.length && !services.filterManager.getGlobalFilters()?.length;
+ if (shouldUpdateWithGlobalFilters) {
+ services.filterManager.setGlobalFilters(globalFilters);
+ }
+
// reset appState in case a saved search with id is loaded and
// the url is empty so the saved search is loaded in a clean
// state else it might be updated by the previous app state
@@ -103,6 +120,10 @@ export const loadSavedSearch = async (
// Update all other services and state containers by the next saved search
updateBySavedSearch(nextSavedSearch, deps);
+ if (!appState && shouldUpdateWithGlobalFilters) {
+ nextSavedSearch = savedSearchContainer.updateWithFilterManagerFilters();
+ }
+
return nextSavedSearch;
};
@@ -125,6 +146,7 @@ function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps
// set data service filters
const filters = savedSearch.searchSource.getField('filter');
if (Array.isArray(filters) && filters.length) {
+ // Saved search SO persists all filters as app filters
services.data.query.filterManager.setAppFilters(cloneDeep(filters));
}
// some filters may not be valid for this context, so update
@@ -134,6 +156,7 @@ function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps
if (!isEqual(currentFilters, validFilters)) {
services.filterManager.setFilters(validFilters);
}
+
// set data service query
const query = savedSearch.searchSource.getField('query');
if (query) {
diff --git a/test/functional/apps/discover/group1/_url_state.ts b/test/functional/apps/discover/group1/_url_state.ts
index 027e767e8fe3d..e97ac332e8b6e 100644
--- a/test/functional/apps/discover/group1/_url_state.ts
+++ b/test/functional/apps/discover/group1/_url_state.ts
@@ -11,6 +11,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const deployment = getService('deployment');
const browser = getService('browser');
const log = getService('log');
const retry = getService('retry');
@@ -19,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
+ const dataGrid = getService('dataGrid');
const PageObjects = getPageObjects([
'common',
'discover',
@@ -30,6 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const defaultSettings = {
defaultIndex: 'logstash-*',
+ hideAnnouncements: true,
};
describe('discover URL state', () => {
@@ -117,5 +120,104 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
expect(await PageObjects.discover.getHitCount()).to.be('11,268');
});
+
+ it('should merge custom global filters with saved search filters', async () => {
+ await kibanaServer.uiSettings.update({
+ 'timepicker:timeDefaults':
+ '{ "from": "Sep 18, 2015 @ 19:37:13.000", "to": "Sep 23, 2015 @ 02:30:09.000"}',
+ });
+ await PageObjects.common.navigateToApp('discover');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ await filterBar.addFilter({
+ field: 'bytes',
+ operation: 'is between',
+ value: { from: '1000', to: '2000' },
+ });
+ await PageObjects.unifiedFieldList.clickFieldListItemAdd('extension');
+ await PageObjects.unifiedFieldList.clickFieldListItemAdd('bytes');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ const totalHitsForOneFilter = '737';
+ const totalHitsForTwoFilters = '137';
+
+ expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter);
+
+ await PageObjects.discover.saveSearch('testFilters');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter);
+
+ await browser.refresh();
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter);
+
+ const url = await browser.getCurrentUrl();
+ const savedSearchIdMatch = url.match(/view\/([^?]+)\?/);
+ const savedSearchId = savedSearchIdMatch?.length === 2 ? savedSearchIdMatch[1] : null;
+
+ expect(typeof savedSearchId).to.be('string');
+
+ await browser.openNewTab();
+ await browser.get(`${deployment.getHostPort()}/app/discover#/view/${savedSearchId}`);
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(await dataGrid.getRowsText()).to.eql([
+ 'Sep 22, 2015 @ 20:44:05.521jpg1,808',
+ 'Sep 22, 2015 @ 20:41:53.463png1,969',
+ 'Sep 22, 2015 @ 20:40:22.952jpg1,576',
+ 'Sep 22, 2015 @ 20:11:39.532png1,708',
+ 'Sep 22, 2015 @ 19:45:13.813php1,406',
+ ]);
+
+ expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter);
+
+ await browser.openNewTab();
+ await browser.get(
+ `${deployment.getHostPort()}/app/discover#/view/${savedSearchId}` +
+ "?_g=(filters:!(('$state':(store:globalState)," +
+ "meta:(alias:!n,disabled:!f,field:extension.raw,index:'logstash-*'," +
+ 'key:extension.raw,negate:!f,params:!(png,css),type:phrases,value:!(png,css)),' +
+ 'query:(bool:(minimum_should_match:1,should:!((match_phrase:(extension.raw:png)),' +
+ "(match_phrase:(extension.raw:css))))))),query:(language:kuery,query:'')," +
+ "refreshInterval:(pause:!t,value:60000),time:(from:'2015-09-19T06:31:44.000Z'," +
+ "to:'2015-09-23T18:31:44.000Z'))"
+ );
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ const filteredRows = [
+ 'Sep 22, 2015 @ 20:41:53.463png1,969',
+ 'Sep 22, 2015 @ 20:11:39.532png1,708',
+ 'Sep 22, 2015 @ 18:50:22.335css1,841',
+ 'Sep 22, 2015 @ 18:40:32.329css1,945',
+ 'Sep 22, 2015 @ 18:13:35.361css1,752',
+ ];
+
+ expect(await dataGrid.getRowsText()).to.eql(filteredRows);
+ expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForTwoFilters);
+ await testSubjects.existOrFail('unsavedChangesBadge');
+
+ await browser.refresh();
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(await dataGrid.getRowsText()).to.eql(filteredRows);
+ expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForTwoFilters);
+ await testSubjects.existOrFail('unsavedChangesBadge');
+ });
});
}
From b62011026850bf968a90c403cae11c453c4e9130 Mon Sep 17 00:00:00 2001
From: Cristina Amico
Date: Thu, 8 Feb 2024 15:59:23 +0100
Subject: [PATCH 021/104] [Fleet] Allow back previously disabled bulk actions
(#176485)
Patch for https://github.com/elastic/kibana/issues/171914
## Summary
In https://github.com/elastic/kibana/pull/175318 I had disabled the bulk
actions when the count was incorrect (negative). This was not a good
idea, since some of those actions could still be applied (like
unenrolling) even if the shown count is not correct. Here I'm removing
that check.
### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../sections/agents/agent_list_page/components/bulk_actions.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx
index cd00c194a6c11..3871473d40bae 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx
@@ -105,7 +105,7 @@ export const AgentBulkActions: React.FunctionComponent = ({
const atLeastOneActiveAgentSelected =
selectionMode === 'manual'
? !!selectedAgents.find((agent) => agent.active)
- : agentCount > 0 && shownAgents > inactiveShownAgents;
+ : shownAgents > inactiveShownAgents;
const agents = selectionMode === 'manual' ? selectedAgents : selectionQuery;
const [tagsPopoverButton, setTagsPopoverButton] = useState();
From 3d431f3816255779ac5d71e88d5ef54a15a533b9 Mon Sep 17 00:00:00 2001
From: Paulo Henrique
Date: Thu, 8 Feb 2024 07:27:09 -0800
Subject: [PATCH 022/104] [Cloud Security] [Posture Dashboard] Update links to
the findings page with groupBy option (#176463)
---
.../common/utils/helpers.ts | 11 ++-
.../public/common/constants.ts | 7 ++
.../common/hooks/use_navigate_findings.ts | 5 +-
.../accounts_evaluated_widget.test.tsx | 69 +++++++++++++++++++
.../components/accounts_evaluated_widget.tsx | 32 +++++----
.../benchmark_details_box.tsx | 34 ++++++---
.../latest_findings/constants.ts | 16 ++---
.../latest_findings_group_renderer.tsx | 11 +--
.../use_latest_findings_grouping.tsx | 21 +++---
9 files changed, 151 insertions(+), 55 deletions(-)
create mode 100644 x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx
diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts
index cbeb334e2aa61..6222b457e7ad6 100644
--- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts
+++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts
@@ -196,6 +196,15 @@ const CLOUD_PROVIDER_NAMES = {
GCP: 'Google Cloud Platform',
};
+export const CLOUD_PROVIDERS = {
+ AWS: 'aws',
+ AZURE: 'azure',
+ GCP: 'gcp',
+};
+
+/**
+ * Returns the cloud provider name or benchmark applicable name for the given benchmark id
+ */
export const getBenchmarkApplicableTo = (benchmarkId: BenchmarksCisId) => {
switch (benchmarkId) {
case 'cis_k8s':
@@ -205,7 +214,7 @@ export const getBenchmarkApplicableTo = (benchmarkId: BenchmarksCisId) => {
case 'cis_aws':
return CLOUD_PROVIDER_NAMES.AWS;
case 'cis_eks':
- return 'Amazon Elastic Kubernetes Service';
+ return 'Amazon Elastic Kubernetes Service (EKS)';
case 'cis_gcp':
return CLOUD_PROVIDER_NAMES.GCP;
}
diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts
index bd266c98b8015..735a28a8ed0e1 100644
--- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts
+++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts
@@ -230,3 +230,10 @@ export const DETECTION_ENGINE_RULES_KEY = 'detection_engine_rules';
export const DETECTION_ENGINE_ALERTS_KEY = 'detection_engine_alerts';
export const DEFAULT_GROUPING_TABLE_HEIGHT = 512;
+
+export const FINDINGS_GROUPING_OPTIONS = {
+ RESOURCE_NAME: 'resource.name',
+ RULE_NAME: 'rule.name',
+ CLOUD_ACCOUNT_NAME: 'cloud.account.name',
+ ORCHESTRATOR_CLUSTER_NAME: 'orchestrator.cluster.name',
+};
diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts
index fbeeeb32a0c2e..4b98057fc10c4 100644
--- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts
+++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts
@@ -57,7 +57,7 @@ const useNavigate = (pathname: string, dataViewId = SECURITY_DEFAULT_DATA_VIEW_I
const { services } = useKibana();
return useCallback(
- (filterParams: NavFilter = {}) => {
+ (filterParams: NavFilter = {}, groupBy?: string[]) => {
const filters = Object.entries(filterParams).map(([key, filterValue]) =>
createFilter(key, filterValue, dataViewId)
);
@@ -68,10 +68,11 @@ const useNavigate = (pathname: string, dataViewId = SECURITY_DEFAULT_DATA_VIEW_I
// Set query language from user's preference
query: services.data.query.queryString.getDefaultQuery(),
filters,
+ ...(groupBy && { groupBy }),
}),
});
},
- [pathname, history, services.data.query.queryString, dataViewId]
+ [history, pathname, services.data.query.queryString, dataViewId]
);
};
diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx
new file mode 100644
index 0000000000000..1973620c8d9d8
--- /dev/null
+++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import { AccountsEvaluatedWidget } from './accounts_evaluated_widget';
+import { BenchmarkData } from '../../common/types_old';
+import { TestProvider } from '../test/test_provider';
+
+const mockNavToFindings = jest.fn();
+jest.mock('../common/hooks/use_navigate_findings', () => ({
+ useNavigateFindings: () => mockNavToFindings,
+}));
+
+describe('AccountsEvaluatedWidget', () => {
+ const benchmarkAssets = [
+ { meta: { benchmarkId: 'cis_aws', assetCount: 10 } },
+ { meta: { benchmarkId: 'cis_k8s', assetCount: 20 } },
+ ] as BenchmarkData[];
+
+ it('renders the component with benchmark data correctly', () => {
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('10')).toBeInTheDocument();
+ expect(getByText('20')).toBeInTheDocument();
+ });
+
+ it('calls navToFindingsByCloudProvider when a benchmark with provider is clicked', () => {
+ const { getByText } = render(
+
+
+
+ );
+
+ fireEvent.click(getByText('10'));
+
+ expect(mockNavToFindings).toHaveBeenCalledWith(
+ {
+ 'cloud.provider': 'aws',
+ },
+ ['cloud.account.name']
+ );
+ });
+
+ it('calls navToFindingsByCisBenchmark when a benchmark with benchmarkId is clicked', () => {
+ const { getByText } = render(
+
+
+
+ );
+
+ fireEvent.click(getByText('20'));
+
+ expect(mockNavToFindings).toHaveBeenCalledWith(
+ {
+ 'rule.benchmark.id': 'cis_k8s',
+ },
+ ['orchestrator.cluster.name']
+ );
+ });
+});
diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx
index a9b118be899ab..aeb734005a65b 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx
@@ -7,38 +7,40 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
+import { CLOUD_PROVIDERS, getBenchmarkApplicableTo } from '../../common/utils/helpers';
import { CIS_AWS, CIS_GCP, CIS_AZURE, CIS_K8S, CIS_EKS } from '../../common/constants';
import { CISBenchmarkIcon } from './cis_benchmark_icon';
import { CompactFormattedNumber } from './compact_formatted_number';
import { useNavigateFindings } from '../common/hooks/use_navigate_findings';
import { BenchmarkData } from '../../common/types_old';
+import { FINDINGS_GROUPING_OPTIONS } from '../common/constants';
// order in array will determine order of appearance in the dashboard
const benchmarks = [
{
type: CIS_AWS,
- name: 'Amazon Web Services (AWS)',
- provider: 'aws',
+ name: getBenchmarkApplicableTo(CIS_AWS),
+ provider: CLOUD_PROVIDERS.AWS,
},
{
type: CIS_GCP,
- name: 'Google Cloud Platform (GCP)',
- provider: 'gcp',
+ name: getBenchmarkApplicableTo(CIS_GCP),
+ provider: CLOUD_PROVIDERS.GCP,
},
{
type: CIS_AZURE,
- name: 'Azure',
- provider: 'azure',
+ name: getBenchmarkApplicableTo(CIS_AZURE),
+ provider: CLOUD_PROVIDERS.AZURE,
},
{
type: CIS_K8S,
- name: 'Kubernetes',
- benchmarkId: 'cis_k8s',
+ name: getBenchmarkApplicableTo(CIS_K8S),
+ benchmarkId: CIS_K8S,
},
{
type: CIS_EKS,
- name: 'Amazon Elastic Kubernetes Service (EKS)',
- benchmarkId: 'cis_eks',
+ name: getBenchmarkApplicableTo(CIS_EKS),
+ benchmarkId: CIS_EKS,
},
];
@@ -59,11 +61,13 @@ export const AccountsEvaluatedWidget = ({
const navToFindings = useNavigateFindings();
const navToFindingsByCloudProvider = (provider: string) => {
- navToFindings({ 'cloud.provider': provider });
+ navToFindings({ 'cloud.provider': provider }, [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]);
};
const navToFindingsByCisBenchmark = (cisBenchmark: string) => {
- navToFindings({ 'rule.benchmark.id': cisBenchmark });
+ navToFindings({ 'rule.benchmark.id': cisBenchmark }, [
+ FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME,
+ ]);
};
const benchmarkElements = benchmarks.map((benchmark) => {
@@ -75,10 +79,10 @@ export const AccountsEvaluatedWidget = ({
key={benchmark.type}
onClick={() => {
if (benchmark.provider) {
- navToFindingsByCloudProvider(benchmark.provider);
+ return navToFindingsByCloudProvider(benchmark.provider);
}
if (benchmark.benchmarkId) {
- navToFindingsByCisBenchmark(benchmark.benchmarkId);
+ return navToFindingsByCisBenchmark(benchmark.benchmarkId);
}
}}
css={css`
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx
index dc140fef121b3..ac9b48ddfdbfe 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx
@@ -17,23 +17,32 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { i18n } from '@kbn/i18n';
+import { FINDINGS_GROUPING_OPTIONS } from '../../../common/constants';
import { getBenchmarkIdQuery } from './benchmarks_section';
import { BenchmarkData } from '../../../../common/types_old';
import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings';
import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon';
import cisLogoIcon from '../../../assets/icons/cis_logo.svg';
+
+interface BenchmarkInfo {
+ name: string;
+ assetType: string;
+ handleClick: () => void;
+}
+
export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) => {
const navToFindings = useNavigateFindings();
- const handleBenchmarkClick = () => {
- return navToFindings(getBenchmarkIdQuery(benchmark));
- };
+ const handleClickCloudProvider = () =>
+ navToFindings(getBenchmarkIdQuery(benchmark), [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]);
+
+ const handleClickCluster = () =>
+ navToFindings(getBenchmarkIdQuery(benchmark), [
+ FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME,
+ ]);
- const getBenchmarkInfo = (
- benchmarkId: string,
- cloudAssetCount: number
- ): { name: string; assetType: string } => {
- const benchmarks: Record = {
+ const getBenchmarkInfo = (benchmarkId: string, cloudAssetCount: number): BenchmarkInfo => {
+ const benchmarks: Record = {
cis_gcp: {
name: i18n.translate(
'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisGcpBenchmarkName',
@@ -48,6 +57,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData })
values: { count: cloudAssetCount },
}
),
+ handleClick: handleClickCloudProvider,
},
cis_aws: {
name: i18n.translate(
@@ -63,6 +73,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData })
values: { count: cloudAssetCount },
}
),
+ handleClick: handleClickCloudProvider,
},
cis_azure: {
name: i18n.translate(
@@ -78,6 +89,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData })
values: { count: cloudAssetCount },
}
),
+ handleClick: handleClickCloudProvider,
},
cis_k8s: {
name: i18n.translate(
@@ -93,6 +105,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData })
values: { count: cloudAssetCount },
}
),
+ handleClick: handleClickCluster,
},
cis_eks: {
name: i18n.translate(
@@ -108,6 +121,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData })
values: { count: cloudAssetCount },
}
),
+ handleClick: handleClickCluster,
},
};
return benchmarks[benchmarkId];
@@ -149,14 +163,14 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData })
}
>
-
+
{benchmarkInfo.name}
-
+
{benchmarkInfo.assetType}
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts
index 3d8200a144bd5..1292da8601357 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts
@@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { GroupOption } from '@kbn/securitysolution-grouping';
+import { FINDINGS_GROUPING_OPTIONS } from '../../../common/constants';
import { FindingsBaseURLQuery } from '../../../common/types';
import { CloudSecurityDefaultColumn } from '../../../components/cloud_security_data_table';
@@ -16,13 +17,6 @@ export const FINDINGS_UNIT = (totalCount: number) =>
defaultMessage: `{totalCount, plural, =1 {finding} other {findings}}`,
});
-export const GROUPING_OPTIONS = {
- RESOURCE_NAME: 'resource.name',
- RULE_NAME: 'rule.name',
- CLOUD_ACCOUNT_NAME: 'cloud.account.name',
- ORCHESTRATOR_CLUSTER_NAME: 'orchestrator.cluster.name',
-};
-
export const NULL_GROUPING_UNIT = i18n.translate('xpack.csp.findings.grouping.nullGroupUnit', {
defaultMessage: 'findings',
});
@@ -51,25 +45,25 @@ export const defaultGroupingOptions: GroupOption[] = [
label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', {
defaultMessage: 'Resource',
}),
- key: GROUPING_OPTIONS.RESOURCE_NAME,
+ key: FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME,
},
{
label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', {
defaultMessage: 'Rule name',
}),
- key: GROUPING_OPTIONS.RULE_NAME,
+ key: FINDINGS_GROUPING_OPTIONS.RULE_NAME,
},
{
label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', {
defaultMessage: 'Cloud account',
}),
- key: GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME,
+ key: FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME,
},
{
label: i18n.translate('xpack.csp.findings.latestFindings.groupByKubernetesCluster', {
defaultMessage: 'Kubernetes cluster',
}),
- key: GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME,
+ key: FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME,
},
];
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx
index fe8536eaf0f69..6dbc78cbf0857 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx
@@ -17,6 +17,7 @@ import { css } from '@emotion/react';
import { GroupPanelRenderer, RawBucket, StatRenderer } from '@kbn/securitysolution-grouping/src';
import React from 'react';
import { i18n } from '@kbn/i18n';
+import { FINDINGS_GROUPING_OPTIONS } from '../../../common/constants';
import {
NullGroup,
LoadingGroup,
@@ -26,7 +27,7 @@ import { getAbbreviatedNumber } from '../../../common/utils/get_abbreviated_numb
import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon';
import { ComplianceScoreBar } from '../../../components/compliance_score_bar';
import { FindingsGroupingAggregation } from './use_grouped_findings';
-import { GROUPING_OPTIONS, NULL_GROUPING_MESSAGES, NULL_GROUPING_UNIT } from './constants';
+import { NULL_GROUPING_MESSAGES, NULL_GROUPING_UNIT } from './constants';
import { FINDINGS_GROUPING_COUNTER } from '../test_subjects';
export const groupPanelRenderer: GroupPanelRenderer = (
@@ -45,7 +46,7 @@ export const groupPanelRenderer: GroupPanelRenderer
);
switch (selectedGroup) {
- case GROUPING_OPTIONS.RESOURCE_NAME:
+ case FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME:
return nullGroupMessage ? (
renderNullGroup(NULL_GROUPING_MESSAGES.RESOURCE_NAME)
) : (
@@ -78,7 +79,7 @@ export const groupPanelRenderer: GroupPanelRenderer
);
- case GROUPING_OPTIONS.RULE_NAME:
+ case FINDINGS_GROUPING_OPTIONS.RULE_NAME:
return nullGroupMessage ? (
renderNullGroup(NULL_GROUPING_MESSAGES.RULE_NAME)
) : (
@@ -100,7 +101,7 @@ export const groupPanelRenderer: GroupPanelRenderer
);
- case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME:
+ case FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME:
return nullGroupMessage ? (
renderNullGroup(NULL_GROUPING_MESSAGES.CLOUD_ACCOUNT_NAME)
) : (
@@ -129,7 +130,7 @@ export const groupPanelRenderer: GroupPanelRenderer
);
- case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME:
+ case FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME:
return nullGroupMessage ? (
renderNullGroup(NULL_GROUPING_MESSAGES.ORCHESTRATOR_CLUSTER_NAME)
) : (
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx
index 74efd68b5378b..81df07731c5dd 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx
@@ -15,7 +15,10 @@ import {
} from '@kbn/securitysolution-grouping/src';
import { useMemo } from 'react';
import { buildEsQuery, Filter } from '@kbn/es-query';
-import { LOCAL_STORAGE_FINDINGS_GROUPING_KEY } from '../../../common/constants';
+import {
+ FINDINGS_GROUPING_OPTIONS,
+ LOCAL_STORAGE_FINDINGS_GROUPING_KEY,
+} from '../../../common/constants';
import { useDataViewContext } from '../../../common/contexts/data_view_context';
import { Evaluation } from '../../../../common/types_old';
import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants';
@@ -24,13 +27,7 @@ import {
FindingsRootGroupingAggregation,
useGroupedFindings,
} from './use_grouped_findings';
-import {
- FINDINGS_UNIT,
- groupingTitle,
- defaultGroupingOptions,
- getDefaultQuery,
- GROUPING_OPTIONS,
-} from './constants';
+import { FINDINGS_UNIT, groupingTitle, defaultGroupingOptions, getDefaultQuery } from './constants';
import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping';
import { getFilters } from '../utils/get_filters';
import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api';
@@ -80,26 +77,26 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => {
];
switch (field) {
- case GROUPING_OPTIONS.RESOURCE_NAME:
+ case FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME:
return [
...aggMetrics,
getTermAggregation('resourceName', 'resource.id'),
getTermAggregation('resourceSubType', 'resource.sub_type'),
getTermAggregation('resourceType', 'resource.type'),
];
- case GROUPING_OPTIONS.RULE_NAME:
+ case FINDINGS_GROUPING_OPTIONS.RULE_NAME:
return [
...aggMetrics,
getTermAggregation('benchmarkName', 'rule.benchmark.name'),
getTermAggregation('benchmarkVersion', 'rule.benchmark.version'),
];
- case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME:
+ case FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME:
return [
...aggMetrics,
getTermAggregation('benchmarkName', 'rule.benchmark.name'),
getTermAggregation('benchmarkId', 'rule.benchmark.id'),
];
- case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME:
+ case FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME:
return [
...aggMetrics,
getTermAggregation('benchmarkName', 'rule.benchmark.name'),
From 7a55b8c94a89bb0b593dcb413e3e0f45a9668d0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yulia=20=C4=8Cech?=
<6585477+yuliacech@users.noreply.github.com>
Date: Thu, 8 Feb 2024 15:34:19 +0000
Subject: [PATCH 023/104] [Index Management] Change the column Components in
the Index templates table (#175823)
## Summary
Fixes https://github.com/elastic/kibana/issues/175690
This PR changes the column Components to display a number of component
templates instead of listing the names, since the text is truncated and
in my opinion is not really readable. The full list of component
templates can still be viewed in the index templates details flyout. The
number of component templates is also a link that redirects to the
Component templates table with a filter initialized in the search bar to
view only those component templates that are used by a specific index
template.
### Screenshots
#### Index templates
Before
After
#### Component templates
Summarize your PR. If it involves visual changes include a screenshot or
gif.
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### Risk Matrix
Delete this section if it is not applicable to this PR.
Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.
When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:
| Risk | Probability | Severity | Mitigation/Notes |
|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../helpers/test_subjects.ts | 3 +-
.../home/index_templates_tab.helpers.ts | 4 +--
.../home/index_templates_tab.test.ts | 29 +++++++++++++++----
.../common/types/templates.ts | 1 +
.../component_template_list.test.ts | 18 ++++++++++++
.../component_template_list.helpers.ts | 20 +++++++++----
.../component_template_list.tsx | 3 ++
.../component_template_list_container.tsx | 11 ++++++-
.../component_template_list/table.tsx | 4 +++
.../template_table/template_table.tsx | 22 ++++++++++----
.../public/application/services/routing.ts | 9 ++++++
.../test/fixtures/template.ts | 2 ++
12 files changed, 107 insertions(+), 19 deletions(-)
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
index 6448b9b79d0a5..c68dcdfc53cec 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
@@ -113,4 +113,5 @@ export type TestSubjects =
| 'createIndexMessage'
| 'indicesSearch'
| 'noIndicesMessage'
- | 'clearIndicesSearch';
+ | 'clearIndicesSearch'
+ | 'componentTemplatesLink';
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
index a56d633217f96..6add533ef57fc 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
@@ -67,7 +67,7 @@ const createActions = (testBed: TestBed) => {
component.update();
};
- const clickTemplateAction = (
+ const clickTemplateAction = async (
templateName: TemplateDeserialized['name'],
action: 'edit' | 'clone' | 'delete'
) => {
@@ -76,7 +76,7 @@ const createActions = (testBed: TestBed) => {
clickActionMenu(templateName);
- act(() => {
+ await act(async () => {
component.find('button.euiContextMenuItem').at(actions.indexOf(action)).simulate('click');
});
component.update();
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index f052317513194..615b8df18f905 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -78,6 +78,26 @@ describe('Index Templates tab', () => {
expect(exists('templateTable')).toBe(true);
expect(exists('legacyTemplateTable')).toBe(false);
});
+
+ test('Components column renders a link to Component templates', async () => {
+ httpRequestsMockHelpers.setLoadTemplatesResponse({
+ templates: [
+ fixtures.getComposableTemplate({
+ name: 'Test',
+ composedOf: ['component1', 'component2'],
+ }),
+ ],
+ legacyTemplates: [],
+ });
+
+ await act(async () => {
+ testBed = await setup(httpSetup);
+ });
+ const { exists, component } = testBed;
+ component.update();
+
+ expect(exists('componentTemplatesLink')).toBe(true);
+ });
});
describe('when there are index templates', () => {
@@ -168,19 +188,18 @@ describe('Index Templates tab', () => {
// Test composable table content
tableCellsValues.forEach((row, i) => {
const indexTemplate = templates[i];
- const { name, indexPatterns, ilmPolicy, composedOf, template } = indexTemplate;
+ const { name, indexPatterns, composedOf, template } = indexTemplate;
const hasContent = !!template?.settings || !!template?.mappings || !!template?.aliases;
- const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : '';
- const composedOfString = composedOf ? composedOf.join(',') : '';
+ const composedOfCount = `${composedOf ? composedOf.length : 0}`;
try {
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
'', // Checkbox to select row
name,
indexPatterns.join(', '),
- ilmPolicyName,
- composedOfString,
+ composedOfCount,
+ '', // data stream column
hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges
'EditDelete', // Column of actions
]);
diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts
index 756d631202445..e2f530b4ad502 100644
--- a/x-pack/plugins/index_management/common/types/templates.ts
+++ b/x-pack/plugins/index_management/common/types/templates.ts
@@ -91,6 +91,7 @@ export interface TemplateListItem {
ilmPolicy?: {
name: string;
};
+ composedOf?: string[];
_kbnMeta: {
type: TemplateType;
hasDatastream: boolean;
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
index c4595998bcd20..a843f9fe28597 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
@@ -173,6 +173,24 @@ describe(' ', () => {
});
});
+ describe('if filter is set, component templates are filtered', () => {
+ test('search value is set if url param is set', async () => {
+ const filter = 'usedBy=(test_index_template_1)';
+ await act(async () => {
+ testBed = await setup(httpSetup, { filter });
+ });
+
+ testBed.component.update();
+
+ const { table } = testBed;
+ const search = testBed.actions.getSearchValue();
+ expect(search).toBe(filter);
+
+ const { rows } = table.getMetaData('componentTemplatesTable');
+ expect(rows.length).toBe(1);
+ });
+ });
+
describe('No component templates', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadComponentTemplatesResponse([]);
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts
index 1bd0152c86d40..8ebc905543a26 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts
@@ -19,13 +19,14 @@ import { BASE_PATH } from '../../../../../../../common';
import { WithAppDependencies } from './setup_environment';
import { ComponentTemplateList } from '../../../component_template_list/component_template_list';
-const testBedConfig: AsyncTestBedConfig = {
+const getTestBedConfig = (props?: any): AsyncTestBedConfig => ({
memoryRouter: {
initialEntries: [`${BASE_PATH}component_templates`],
componentRoutePath: `${BASE_PATH}component_templates`,
},
doMountAsync: true,
-};
+ defaultProps: props,
+});
export type ComponentTemplateListTestBed = TestBed & {
actions: ReturnType;
@@ -73,18 +74,26 @@ const createActions = (testBed: TestBed) => {
deleteButton.simulate('click');
};
+ const getSearchValue = () => {
+ return find('componentTemplatesSearch').prop('defaultValue');
+ };
+
return {
clickReloadButton,
clickComponentTemplateAt,
clickDeleteActionAt,
clickTableColumnSortButton,
+ getSearchValue,
};
};
-export const setup = async (httpSetup: HttpSetup): Promise => {
+export const setup = async (
+ httpSetup: HttpSetup,
+ props?: any
+): Promise => {
const initTestBed = registerTestBed(
WithAppDependencies(ComponentTemplateList, httpSetup),
- testBedConfig
+ getTestBedConfig(props)
);
const testBed = await initTestBed();
@@ -104,7 +113,8 @@ export type ComponentTemplateTestSubjects =
| 'sectionLoading'
| 'componentTemplatesLoadError'
| 'deleteComponentTemplateButton'
- | 'deprecatedComponentTemplateBadge'
| 'reloadButton'
+ | 'componentTemplatesSearch'
+ | 'deprecatedComponentTemplateBadge'
| 'componentTemplatesFiltersButton'
| 'componentTemplates--deprecatedFilter';
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
index fc309751bfe8d..65e369cc8c880 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
@@ -36,6 +36,7 @@ import { useRedirectPath } from '../../../hooks/redirect_path';
interface Props {
componentTemplateName?: string;
history: RouteComponentProps['history'];
+ filter?: string;
}
const { useGlobalFlyout } = GlobalFlyout;
@@ -43,6 +44,7 @@ const { useGlobalFlyout } = GlobalFlyout;
export const ComponentTemplateList: React.FunctionComponent = ({
componentTemplateName,
history,
+ filter,
}) => {
const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } =
useGlobalFlyout();
@@ -183,6 +185,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({
{
const { executionContext } = useComponentTemplatesContext();
@@ -33,10 +35,17 @@ export const ComponentTemplateListContainer: React.FunctionComponent<
page: 'indexManagementComponentTemplatesTab',
});
+ const urlParams = qs.parse(location.search);
+ const filter = urlParams.filter ?? '';
+
return (
-
+
);
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
index f4d9c55407fd9..7f6eb87566410 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
@@ -51,6 +51,7 @@ const deprecatedFilterLabel = i18n.translate(
export interface Props {
componentTemplates: ComponentTemplateListItem[];
+ defaultFilter: string;
onReloadClick: () => void;
onDeleteClick: (componentTemplateName: string[]) => void;
onEditClick: (componentTemplateName: string) => void;
@@ -60,6 +61,7 @@ export interface Props {
export const ComponentTable: FunctionComponent = ({
componentTemplates,
+ defaultFilter,
onReloadClick,
onDeleteClick,
onEditClick,
@@ -188,6 +190,7 @@ export const ComponentTable: FunctionComponent = ({
],
box: {
incremental: true,
+ 'data-test-subj': 'componentTemplatesSearch',
},
filters: [
{
@@ -220,6 +223,7 @@ export const ComponentTable: FunctionComponent = ({
},
},
],
+ defaultQuery: defaultFilter,
},
pagination: {
initialPageSize: 10,
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx
index a0b4f07e61722..4a08a93c9a0c4 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx
@@ -18,8 +18,8 @@ import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_i
import { useServices } from '../../../../app_context';
import { TemplateDeleteModal } from '../../../../components';
import { TemplateContentIndicator } from '../../../../components/shared';
+import { getComponentTemplatesLink, getTemplateDetailsLink } from '../../../../services/routing';
import { TemplateTypeIndicator, TemplateDeprecatedBadge } from '../components';
-import { getTemplateDetailsLink } from '../../../../services/routing';
interface Props {
templates: TemplateListItem[];
@@ -85,12 +85,24 @@ export const TemplateTable: React.FunctionComponent = ({
{
field: 'composedOf',
name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', {
- defaultMessage: 'Components',
+ defaultMessage: 'Component templates',
}),
+ width: '100px',
truncateText: true,
- sortable: true,
- width: '20%',
- render: (composedOf: string[] = []) => {composedOf.join(', ')} ,
+ sortable: (template) => {
+ return template.composedOf?.length;
+ },
+ render: (composedOf: string[] = [], item: TemplateListItem) =>
+ composedOf.length === 0 ? (
+ 0
+ ) : (
+
+ {composedOf.length}
+
+ ),
},
{
name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', {
diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts
index a2d4a03013556..07653d2591ffc 100644
--- a/x-pack/plugins/index_management/public/application/services/routing.ts
+++ b/x-pack/plugins/index_management/public/application/services/routing.ts
@@ -69,3 +69,12 @@ export const getIndexDetailsLink = (
}
return link;
};
+
+export const getComponentTemplatesLink = (usedByTemplateName?: string) => {
+ let url = '/component_templates';
+ if (usedByTemplateName) {
+ const filter = `usedBy=(${usedByTemplateName})`;
+ url = `${url}?filter=${encodeURIComponent(filter)}`;
+ }
+ return url;
+};
diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts
index 01f83e2d76ff5..f7db386095b07 100644
--- a/x-pack/plugins/index_management/test/fixtures/template.ts
+++ b/x-pack/plugins/index_management/test/fixtures/template.ts
@@ -23,6 +23,7 @@ export const getComposableTemplate = ({
isLegacy = false,
type = 'default',
allowAutoCreate = false,
+ composedOf = [],
}: Partial<
TemplateDeserialized & {
isLegacy?: boolean;
@@ -53,6 +54,7 @@ export const getComposableTemplate = ({
hasDatastream,
isLegacy,
},
+ composedOf,
};
return indexTemplate;
From ebb387692c397e1eb1636ee8f0005b179a99424c Mon Sep 17 00:00:00 2001
From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Date: Thu, 8 Feb 2024 16:38:35 +0100
Subject: [PATCH 024/104] [Fleet] more detailed error message on top-level
manifest archive error (#176492)
Logging more specific info in the error message about which manifest
file is not found.
---
.../plugins/fleet/server/services/epm/archive/parse.test.ts | 2 +-
x-pack/plugins/fleet/server/services/epm/archive/parse.ts | 4 +++-
.../test/fleet_api_integration/apis/epm/install_by_upload.ts | 2 +-
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts b/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts
index ac72f56946d04..f8f2734444b25 100644
--- a/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts
@@ -408,7 +408,7 @@ describe('parseAndVerifyArchive', () => {
it('should throw on missing manifest file', () => {
expect(() => parseAndVerifyArchive(['input_only-0.1.0/test/manifest.yml'], {})).toThrowError(
new PackageInvalidArchiveError(
- 'Package at top-level directory input_only-0.1.0 must contain a top-level manifest.yml file.'
+ 'Manifest file input_only-0.1.0/manifest.yml not found in paths.'
)
);
});
diff --git a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts
index b7a2b8a26400c..b0b5d8a94f06f 100644
--- a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts
+++ b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts
@@ -220,7 +220,9 @@ export function parseAndVerifyArchive(
logger.debug(`Verifying archive - checking manifest file and manifest buffer`);
if (!paths.includes(manifestFile) || !manifestBuffer) {
throw new PackageInvalidArchiveError(
- `Package at top-level directory ${toplevelDir} must contain a top-level ${MANIFEST_NAME} file.`
+ !paths.includes(manifestFile)
+ ? `Manifest file ${manifestFile} not found in paths.`
+ : `Manifest buffer is not found in assets map for manifest file ${manifestFile}.`
);
}
diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts
index c34413aaae2c2..ac741e10db1e1 100644
--- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts
+++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts
@@ -207,7 +207,7 @@ export default function (providerContext: FtrProviderContext) {
.send(buf)
.expect(400);
expect(res.error.text).to.equal(
- '{"statusCode":400,"error":"Bad Request","message":"Package at top-level directory apache-0.1.4 must contain a top-level manifest.yml file."}'
+ '{"statusCode":400,"error":"Bad Request","message":"Manifest file apache-0.1.4/manifest.yml not found in paths."}'
);
});
From bb11c087a4e18846aa049e79c164b867b86c3b3f Mon Sep 17 00:00:00 2001
From: Jill Guyonnet
Date: Thu, 8 Feb 2024 15:43:13 +0000
Subject: [PATCH 025/104] [Fleet] Add serverless dev docs (#176304)
## Summary
Add Fleet dev docs with detailed instructions for running Kibana in
serverless mode.
---
x-pack/plugins/fleet/README.md | 4 +
.../developing_kibana_and_fleet_server.md | 9 +-
.../developing_kibana_in_serverless.md | 122 ++++++++++++++++++
3 files changed, 132 insertions(+), 3 deletions(-)
create mode 100644 x-pack/plugins/fleet/dev_docs/developing_kibana_in_serverless.md
diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md
index 07dd4bfd67c00..4fd48aa82aff6 100644
--- a/x-pack/plugins/fleet/README.md
+++ b/x-pack/plugins/fleet/README.md
@@ -38,6 +38,8 @@ Note: The plugin was previously named Ingest Manager, it's possible that some va
These are some additional recommendations to the steps detailed in the [Kibana Developer Guide](https://docs.elastic.dev/kibana-dev-docs/getting-started/setup-dev-env).
+Note: this section details how to run Kibana in stateful mode. For serverless development, see the [Developing Kibana in serverless mode](dev_docs/developing_kibana_and_fleet_server.md) guide.
+
1. Create a `config/kibana.dev.yml` file by copying the existing `config/kibana.yml` file.
2. It is recommended to explicitly set a base path for Kibana (refer to [Considerations for basepath](https://www.elastic.co/guide/en/kibana/current/development-basepath.html) for details). To do this, add the following to your `kibana.dev.yml`:
@@ -93,6 +95,8 @@ Refer to the [Running Elasticsearch during development](https://www.elastic.co/g
It can be useful to run Fleet Server in a container on your local machine in order to free up your actual "bare metal" machine to run Elastic Agent for testing purposes. Otherwise, you'll only be able to a single instance of Elastic Agent dedicated to Fleet Server on your local machine, and this can make testing integrations and policies difficult.
+Note: if you need to do simultaneous Kibana and Fleet Server development, refer to the [Developing Kibana and Fleet Server simulatanously](dev_docs/developing_kibana_and_fleet_server.md) guide.
+
_The following is adapted from the Fleet Server [README](https://github.com/elastic/fleet-server#running-elastic-agent-with-fleet-server-in-container)_
1. Add the following configuration to your `kibana.dev.yml`
diff --git a/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md b/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md
index 376d6cbb739e6..745f205c72a03 100644
--- a/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md
+++ b/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md
@@ -402,11 +402,14 @@ _To do: add specific docs for enrolling Multipass agents and link here_
## Running in serverless mode
-If you want to run your local stack in serverless mode, you'll only need to alter the commands used to start Elasticsearch and Kibana. Fleet Server does not require any changes outside of what's listed above to run in a serverless context. From your Kibana, start a serverless Elasticsearch snapshot, and then run Kibana as either a security or observability project.
+If you want to run your local stack in serverless mode, you'll only need to alter the commands used to start Elasticsearch and Kibana. Fleet Server does not require any changes outside of what's listed above to run in a serverless context. From your Kibana, start a serverless Elasticsearch snapshot as either a security or observability project, and then run Kibana using the same project type.
```bash
-# Start Elasticsearch in serverless mode
-yarn es serverless --kill
+# Start Elasticsearch in serverless mode as a security project
+yarn es serverless --projectType=security --kill
+
+# Start Elasticsearch in serverless mode as an observability project
+yarn es serverless --projectType=oblt --kill
# Run kibana as a security project
yarn serverless-security
diff --git a/x-pack/plugins/fleet/dev_docs/developing_kibana_in_serverless.md b/x-pack/plugins/fleet/dev_docs/developing_kibana_in_serverless.md
new file mode 100644
index 0000000000000..f52ec7bcec700
--- /dev/null
+++ b/x-pack/plugins/fleet/dev_docs/developing_kibana_in_serverless.md
@@ -0,0 +1,122 @@
+# Developing Kibana in serverless mode
+
+Fleet is enabled for the observability and security serverless project types.
+
+To run Elasticsearch and Kibana in serverless mode, the relevant commands are:
+
+For the observability project type:
+```bash
+# Start Elasticsearch in serverless mode as an observability project
+yarn es serverless --projectType=oblt --kill
+
+# Run Kibana as an observability project
+yarn serverless-oblt
+```
+
+and one of:
+
+```bash
+# Start Elasticsearch in serverless mode as a security project
+yarn es serverless --projectType=security --kill
+
+# Run Kibana as a security project
+yarn serverless-security
+```
+
+Once running, you can login at `http://localhost:5601` with the username `elastic_serverless` or `system_indices_superuser` and the password `changeme`.
+
+Note: it is not possible to use a base path in serverless mode. In case of issue, make sure the `server.basePath` property is not set in the config.
+
+Tip: to reset Elasticsearch data, delete the following folder:
+
+```bash
+# Run this from the kibana folder:
+rm -rf .es/stateless
+```
+
+## Kibana config
+
+### Setting a project id
+
+Create a `config/serverless.dev.yml` config file if you don't already have one and add a project id:
+
+```yaml
+xpack.cloud.serverless.project_id: test-123
+```
+
+The project id is required for some functionality, such as the `isServerless` flag in Fleet's cloud setup.
+
+### Fleet config
+
+The `kibana.dev.yml` settings should be mostly the same as for stateful mode. There are however a few key differences.
+
+As noted above, the base path should not be set (`server.basePath` setting).
+
+To enroll agents with a standalone Fleet Server set:
+```yaml
+xpack.fleet.internal.fleetServerStandalone: true
+```
+
+Finally, it may be necessary for the Fleet config to accurately reflect the generated config for serverless projects, which is defined in [project-controller](https://github.com/elastic/project-controller/blob/69dc1e6b0761bd9c933c23c2a471f32e1b8f1d28/internal/application/kibana/fleet_config.go#L43). At the time of writing, this concerns the default Fleet Server host and default output:
+
+```yaml
+xpack.fleet.fleetServerHosts:
+ - id: default-fleet-server
+ name: Default Fleet server
+ is_default: true
+ host_urls: ['http://localhost:8220']
+ # If you want to run a Fleet Server containers via Docker, use this URL:
+ # host_urls: ['https://host.docker.internal:8220']
+xpack.fleet.outputs:
+ - id: es-default-output
+ name: Default output
+ type: elasticsearch
+ is_default: true
+ is_default_monitoring: true
+ hosts: ['https://localhost:9200']
+ # # If you enroll agents via Docker, use this URL:
+ # hosts: ['https://host.docker.internal:9200']
+```
+
+## Running a Fleet Server and enrolling agents
+
+In serverless mode, Fleet Server runs in standalone mode. Unless you are [simultaneously developing Kibana and Fleet Server](./developing_kibana_and_fleet_server.mddeveloping_), it is easier to run Fleet Server as a Docker container.
+
+The Kibana's dev utils package defines a hard-coded [Fleet Server service token](ttps://github.com/elastic/kibana/blob/92b6fd64cd58fd62f69898c222e86409d5f15b60/packages/kbn-dev-utils/src/dev_service_account.ts#L21-L25) and fingerprint of the ca.crt certificate.
+
+Running a standalone Fleet Server:
+
+```bash
+docker run -it --rm \
+ -e ELASTICSEARCH_HOSTS="http://host.docker.internal:9200" \
+ -e ELASTICSEARCH_SERVICE_TOKEN="AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL2ZsZWV0LXNlcnZlci1kZXY6VVo1TWd6MnFTX3FVTWliWGNXNzlwQQ" \
+ -e ELASTICSEARCH_CA_TRUSTED_FINGERPRINT="F71F73085975FD977339A1909EBFE2DF40DB255E0D5BB56FC37246BF383FFC84" \
+ -p 8220:8220 \
+ docker.elastic.co/observability-ci/fleet-server:latest
+```
+
+Containerized elastic agents can then be enrolled using:
+
+```bash
+docker run \
+ -e FLEET_URL=http://host.docker.internal:8220 \
+ -e FLEET_ENROLL=1 \
+ -e FLEET_ENROLLMENT_TOKEN=== \
+ -e FLEET_INSECURE=1 \
+ --rm docker.elastic.co/beats/elastic-agent:
+```
+
+## Troubleshooting
+
+### `Cannot read existing Message Signing Key pair` issue
+
+At the time of writing, there is a known issue where Fleet may fail to load due to error generating key pair for message signing. This may in particular happen after running API integration tests. The easiest solution in development is to reset Elasticsearch data:
+
+```bash
+# Run this from the kibana folder:
+rm -rf .es/stateless
+```
+
+## Release
+
+Serverless Kibana is periodically released from `main` following a deployment workflow composed of four environments (CI, QA, Staging, Production). It is therefore important to be aware of the release schedule and ensure timely communication with our QA team prior to merging.
From f50e5bb8b7efb8fade7f8c1dac74de0e40c04d93 Mon Sep 17 00:00:00 2001
From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
Date: Thu, 8 Feb 2024 16:44:34 +0100
Subject: [PATCH 026/104] [Cases] Update custom field internal api (#176168)
## Summary
Related to https://github.com/elastic/kibana/issues/175931
This PR creates an internal API to update single custom field of a case
Path: `PATCH /internal/cases//custom_fields/{customFieldId}`
Body:
`{
"customFieldDetails": {
"type": customField type,
"value": customField value,
},
"version": Cases version
}`
**Flaky test runner:**
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5065
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../plugins/cases/common/constants/index.ts | 2 +-
.../plugins/cases/common/types/api/case/v1.ts | 9 +-
.../common/types/api/custom_field/v1.test.ts | 64 ++-
.../cases/common/types/api/custom_field/v1.ts | 12 +
.../cases/server/client/cases/client.ts | 10 +-
.../client/cases/replace_custom_field.test.ts | 394 +++++++++++++++
.../client/cases/replace_custom_field.ts | 169 +++++++
x-pack/plugins/cases/server/client/mocks.ts | 1 +
.../server/routes/api/get_internal_routes.ts | 2 +
.../api/internal/replace_custom_field.ts | 48 ++
.../common/lib/api/index.ts | 37 ++
.../security_and_spaces/tests/common/index.ts | 1 +
.../common/internal/replace_custom_field.ts | 464 ++++++++++++++++++
13 files changed, 1206 insertions(+), 7 deletions(-)
create mode 100644 x-pack/plugins/cases/server/client/cases/replace_custom_field.test.ts
create mode 100644 x-pack/plugins/cases/server/client/cases/replace_custom_field.ts
create mode 100644 x-pack/plugins/cases/server/routes/api/internal/replace_custom_field.ts
create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/replace_custom_field.ts
diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts
index ba7e98d775efb..50dda0c185176 100644
--- a/x-pack/plugins/cases/common/constants/index.ts
+++ b/x-pack/plugins/cases/common/constants/index.ts
@@ -82,7 +82,7 @@ export const INTERNAL_DELETE_FILE_ATTACHMENTS_URL =
export const INTERNAL_GET_CASE_CATEGORIES_URL = `${CASES_INTERNAL_URL}/categories` as const;
export const INTERNAL_CASE_METRICS_URL = `${CASES_INTERNAL_URL}/metrics` as const;
export const INTERNAL_CASE_METRICS_DETAILS_URL = `${CASES_INTERNAL_URL}/metrics/{case_id}` as const;
-
+export const INTERNAL_PUT_CUSTOM_FIELDS_URL = `${CASES_INTERNAL_URL}/{case_id}/custom_fields/{custom_field_id}`;
/**
* Action routes
*/
diff --git a/x-pack/plugins/cases/common/types/api/case/v1.ts b/x-pack/plugins/cases/common/types/api/case/v1.ts
index acb8049e01737..0dff1cac0d95d 100644
--- a/x-pack/plugins/cases/common/types/api/case/v1.ts
+++ b/x-pack/plugins/cases/common/types/api/case/v1.ts
@@ -51,7 +51,7 @@ const CaseCustomFieldTextWithValidationRt = rt.strict({
const CustomFieldRt = rt.union([CaseCustomFieldTextWithValidationRt, CaseCustomFieldToggleRt]);
-const CustomFieldsRt = limitedArraySchema({
+export const CaseRequestCustomFieldsRt = limitedArraySchema({
codec: CustomFieldRt,
fieldName: 'customFields',
min: 0,
@@ -124,7 +124,7 @@ export const CasePostRequestRt = rt.intersection([
/**
* The list of custom field values of the case.
*/
- customFields: CustomFieldsRt,
+ customFields: CaseRequestCustomFieldsRt,
})
),
]);
@@ -418,7 +418,7 @@ export const CasePatchRequestRt = rt.intersection([
/**
* Custom fields of the case
*/
- customFields: CustomFieldsRt,
+ customFields: CaseRequestCustomFieldsRt,
})
),
/**
@@ -510,6 +510,7 @@ export type GetReportersResponse = rt.TypeOf;
export type CasesBulkGetRequest = rt.TypeOf;
export type CasesBulkGetResponse = rt.TypeOf;
export type GetRelatedCasesByAlertResponse = rt.TypeOf;
-export type CaseRequestCustomFields = rt.TypeOf;
+export type CaseRequestCustomFields = rt.TypeOf;
+export type CaseRequestCustomField = rt.TypeOf;
export type BulkCreateCasesRequest = rt.TypeOf;
export type BulkCreateCasesResponse = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts b/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts
index e2f54761d6670..83d9a437c998d 100644
--- a/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts
@@ -7,7 +7,7 @@
import { PathReporter } from 'io-ts/lib/PathReporter';
import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants';
-import { CaseCustomFieldTextWithValidationValueRt } from './v1';
+import { CaseCustomFieldTextWithValidationValueRt, CustomFieldPutRequestRt } from './v1';
describe('Custom Fields', () => {
describe('CaseCustomFieldTextWithValidationValueRt', () => {
@@ -38,4 +38,66 @@ describe('Custom Fields', () => {
);
});
});
+
+ describe('CustomFieldPutRequestRt', () => {
+ const defaultRequest = {
+ caseVersion: 'WzQ3LDFd',
+ value: 'this is a text field value',
+ };
+
+ it('has expected attributes in request', () => {
+ const query = CustomFieldPutRequestRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+
+ it('has expected attributes of toggle field in request', () => {
+ const newRequest = {
+ caseVersion: 'WzQ3LDFd',
+ value: false,
+ };
+ const query = CustomFieldPutRequestRt.decode(newRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: newRequest,
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = CustomFieldPutRequestRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+
+ it(`throws an error when a text customField is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => {
+ expect(
+ PathReporter.report(
+ CustomFieldPutRequestRt.decode({
+ caseVersion: 'WzQ3LDFd',
+ value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1),
+ })
+ )
+ ).toContain(
+ `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.`
+ );
+ });
+
+ it('throws an error when a text customField is empty', () => {
+ expect(
+ PathReporter.report(
+ CustomFieldPutRequestRt.decode({
+ caseVersion: 'WzQ3LDFd',
+ value: '',
+ })
+ )
+ ).toContain('The value field cannot be an empty string.');
+ });
+ });
});
diff --git a/x-pack/plugins/cases/common/types/api/custom_field/v1.ts b/x-pack/plugins/cases/common/types/api/custom_field/v1.ts
index 4ee70642c86b1..fb59f187991b3 100644
--- a/x-pack/plugins/cases/common/types/api/custom_field/v1.ts
+++ b/x-pack/plugins/cases/common/types/api/custom_field/v1.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import * as rt from 'io-ts';
import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants';
import { limitedStringSchema } from '../../../schema';
@@ -14,3 +15,14 @@ export const CaseCustomFieldTextWithValidationValueRt = (fieldName: string) =>
min: 1,
max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH,
});
+
+/**
+ * Update custom_field
+ */
+
+export const CustomFieldPutRequestRt = rt.strict({
+ value: rt.union([rt.boolean, rt.null, CaseCustomFieldTextWithValidationValueRt('value')]),
+ caseVersion: rt.string,
+});
+
+export type CustomFieldPutRequest = rt.TypeOf;
diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts
index 2c19df9b4c0f1..4819ca3fc1672 100644
--- a/x-pack/plugins/cases/server/client/cases/client.ts
+++ b/x-pack/plugins/cases/server/client/cases/client.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import type { Case, Cases, User } from '../../../common/types/domain';
+import type { Case, CaseCustomField, Cases, User } from '../../../common/types/domain';
import type {
CasePostRequest,
CasesFindResponse,
@@ -34,6 +34,8 @@ import type { PushParams } from './push';
import { push } from './push';
import { update } from './update';
import { bulkCreate } from './bulk_create';
+import type { ReplaceCustomFieldArgs } from './replace_custom_field';
+import { replaceCustomField } from './replace_custom_field';
/**
* API for interacting with the cases entities.
@@ -96,6 +98,10 @@ export interface CasesSubClient {
* Retrieves the cases ID and title that have the requested alert attached to them
*/
getCasesByAlertID(params: CasesByAlertIDParams): Promise;
+ /**
+ * Replace custom field with specific customFieldId and CaseId
+ */
+ replaceCustomField(params: ReplaceCustomFieldArgs): Promise;
}
/**
@@ -122,6 +128,8 @@ export const createCasesSubClient = (
getCategories: (params: AllCategoriesFindRequest) => getCategories(params, clientArgs),
getReporters: (params: AllReportersFindRequest) => getReporters(params, clientArgs),
getCasesByAlertID: (params: CasesByAlertIDParams) => getCasesByAlertID(params, clientArgs),
+ replaceCustomField: (params: ReplaceCustomFieldArgs) =>
+ replaceCustomField(params, clientArgs, casesClient),
};
return Object.freeze(casesSubClient);
diff --git a/x-pack/plugins/cases/server/client/cases/replace_custom_field.test.ts b/x-pack/plugins/cases/server/client/cases/replace_custom_field.test.ts
new file mode 100644
index 0000000000000..f4c3666db7083
--- /dev/null
+++ b/x-pack/plugins/cases/server/client/cases/replace_custom_field.test.ts
@@ -0,0 +1,394 @@
+/*
+ * 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 { CustomFieldTypes } from '../../../common/types/domain';
+import { MAX_USER_ACTIONS_PER_CASE } from '../../../common/constants';
+import { mockCases } from '../../mocks';
+import { createCasesClientMock, createCasesClientMockArgs } from '../mocks';
+import { replaceCustomField } from './replace_custom_field';
+
+describe('Replace custom field', () => {
+ const customFields = [
+ {
+ key: 'first_key',
+ type: CustomFieldTypes.TEXT as const,
+ value: 'this is a text field value',
+ },
+ {
+ key: 'second_key',
+ type: CustomFieldTypes.TOGGLE as const,
+ value: null,
+ },
+ ];
+
+ const theCase = { ...mockCases[0], attributes: { ...mockCases[0].attributes, customFields } };
+ const clientArgs = createCasesClientMockArgs();
+ const casesClient = createCasesClientMock();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ clientArgs.services.caseService.getCase.mockResolvedValue(theCase);
+ clientArgs.services.userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({
+ [mockCases[0].id]: 1,
+ });
+
+ casesClient.configure.get = jest.fn().mockResolvedValue([
+ {
+ owner: mockCases[0].attributes.owner,
+ customFields: [
+ {
+ key: 'first_key',
+ type: CustomFieldTypes.TEXT,
+ label: 'missing field 1',
+ required: true,
+ },
+ {
+ key: 'second_key',
+ type: CustomFieldTypes.TOGGLE,
+ label: 'foo',
+ required: false,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('can replace text customField', async () => {
+ clientArgs.services.caseService.patchCase.mockResolvedValue({
+ ...theCase,
+ });
+
+ await expect(
+ replaceCustomField(
+ {
+ caseId: theCase.id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: 'Updated text field value',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).resolves.not.toThrow();
+
+ expect(clientArgs.services.caseService.patchCase).toHaveBeenCalledWith(
+ expect.objectContaining({
+ caseId: theCase.id,
+ version: theCase.version,
+ originalCase: {
+ ...theCase,
+ },
+ updatedAttributes: {
+ customFields: [
+ {
+ key: 'first_key',
+ type: CustomFieldTypes.TEXT as const,
+ value: 'Updated text field value',
+ },
+ {
+ key: 'second_key',
+ type: CustomFieldTypes.TOGGLE as const,
+ value: null,
+ },
+ ],
+ updated_at: expect.any(String),
+ updated_by: expect.any(Object),
+ },
+ refresh: false,
+ })
+ );
+ });
+
+ it('can replace toggle customField', async () => {
+ clientArgs.services.caseService.patchCase.mockResolvedValue({
+ ...theCase,
+ });
+
+ await expect(
+ replaceCustomField(
+ {
+ caseId: theCase.id,
+ customFieldId: 'second_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: true,
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).resolves.not.toThrow();
+
+ expect(clientArgs.services.caseService.patchCase).toHaveBeenCalledWith(
+ expect.objectContaining({
+ caseId: theCase.id,
+ version: theCase.version,
+ originalCase: {
+ ...theCase,
+ },
+ updatedAttributes: {
+ customFields: [
+ {
+ key: 'second_key',
+ type: CustomFieldTypes.TOGGLE as const,
+ value: true,
+ },
+ {
+ key: 'first_key',
+ type: CustomFieldTypes.TEXT as const,
+ value: 'this is a text field value',
+ },
+ ],
+ updated_at: expect.any(String),
+ updated_by: expect.any(Object),
+ },
+ refresh: false,
+ })
+ );
+ });
+
+ it('does not throw error when customField value is null and the custom field is not required', async () => {
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'second_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: null,
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).resolves.not.toThrow();
+ });
+
+ it('throws error when request is invalid', async () => {
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ // @ts-expect-error check for invalid attribute
+ foo: 'bar',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"undefined\\" supplied to \\"value\\""`
+ );
+ });
+
+ it('throws error when case version does not match', async () => {
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: 'random-version',
+ value: 'test',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: first_key of case: mock-id-1 version:random-version : Error: This case mock-id-1 has been updated. Please refresh before saving additional updates."`
+ );
+ });
+
+ it('throws error when customField value is null and the custom field is required', async () => {
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: null,
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Custom field value cannot be null or undefined."`
+ );
+ });
+
+ it('throws error when required customField of type text has value as empty string', async () => {
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: ' ',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\" \\" supplied to \\"value\\",The value field cannot be an empty string."`
+ );
+ });
+
+ it('throws error when customField value is undefined and the custom field is required', async () => {
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ // @ts-expect-error: undefined value
+ value: undefined,
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"undefined\\" supplied to \\"value\\""`
+ );
+ });
+
+ it('throws error when customField key is not present in configuration', async () => {
+ clientArgs.services.caseService.getCase.mockResolvedValue(mockCases[0]);
+
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'missing_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: 'updated',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: missing_key of case: mock-id-1 version:WzAsMV0= : Error: cannot find custom field"`
+ );
+ });
+
+ it('throws error when the customField type does not match the configuration', async () => {
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'second_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: 'foobar',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: second_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"foobar\\" supplied to \\"value\\""`
+ );
+ });
+
+ it('throws error when the customField not found after update', async () => {
+ clientArgs.services.caseService.patchCase.mockResolvedValue({
+ ...theCase,
+ attributes: {
+ ...theCase.attributes,
+ customFields: [],
+ },
+ });
+
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'second_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: false,
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Failed to replace customField, id: second_key of case: mock-id-1 version:WzAsMV0= : Error: Cannot find updated custom field."`
+ );
+ });
+
+ describe('Validate max user actions', () => {
+ it('passes validation if max user actions per case is not reached', async () => {
+ clientArgs.services.userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({
+ [mockCases[0].id]: MAX_USER_ACTIONS_PER_CASE - 1,
+ });
+
+ // @ts-ignore: only the array length matters here
+ clientArgs.services.userActionService.creator.buildUserActions.mockReturnValue({
+ [mockCases[0].id]: [1],
+ });
+
+ clientArgs.services.caseService.patchCase.mockResolvedValue(theCase);
+
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: 'foobar',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).resolves.not.toThrow();
+ });
+
+ it(`throws an error when the user actions to be created will reach ${MAX_USER_ACTIONS_PER_CASE}`, async () => {
+ clientArgs.services.userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({
+ [mockCases[0].id]: MAX_USER_ACTIONS_PER_CASE,
+ });
+
+ // @ts-ignore: only the array length matters here
+ clientArgs.services.userActionService.creator.buildUserActions.mockReturnValue({
+ [mockCases[0].id]: [1, 2, 3],
+ });
+
+ await expect(
+ replaceCustomField(
+ {
+ caseId: mockCases[0].id,
+ customFieldId: 'first_key',
+ request: {
+ caseVersion: mockCases[0].version ?? '',
+ value: 'foobar',
+ },
+ },
+ clientArgs,
+ casesClient
+ )
+ ).rejects.toThrow(
+ `Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: The case with id mock-id-1 has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.`
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/server/client/cases/replace_custom_field.ts b/x-pack/plugins/cases/server/client/cases/replace_custom_field.ts
new file mode 100644
index 0000000000000..5a87c2acc1bf9
--- /dev/null
+++ b/x-pack/plugins/cases/server/client/cases/replace_custom_field.ts
@@ -0,0 +1,169 @@
+/*
+ * 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 Boom from '@hapi/boom';
+
+import type { CasesClient, CasesClientArgs } from '..';
+
+import type { CustomFieldPutRequest } from '../../../common/types/api';
+import { CustomFieldPutRequestRt, CaseRequestCustomFieldsRt } from '../../../common/types/api';
+import { Operations } from '../../authorization';
+import { createCaseError } from '../../common/error';
+import { validateMaxUserActions } from '../../../common/utils/validators';
+import { decodeOrThrow } from '../../../common/api/runtime_types';
+import type { CaseCustomField } from '../../../common/types/domain';
+import { CaseCustomFieldRt } from '../../../common/types/domain';
+import { decodeWithExcessOrThrow } from '../../../common/api';
+import { validateCustomFieldTypesInRequest } from './validators';
+import type { UserActionEvent } from '../../services/user_actions/types';
+
+export interface ReplaceCustomFieldArgs {
+ /**
+ * The ID of a case
+ */
+ caseId: string;
+ /**
+ * The ID of a custom field to be updated
+ */
+ customFieldId: string;
+ /**
+ * value of custom field to update, case version
+ */
+ request: CustomFieldPutRequest;
+}
+
+/**
+ * Updates the specified cases with new values
+ *
+ * @ignore
+ */
+export const replaceCustomField = async (
+ { caseId, customFieldId, request }: ReplaceCustomFieldArgs,
+ clientArgs: CasesClientArgs,
+ casesClient: CasesClient
+): Promise => {
+ const {
+ services: { caseService, userActionService },
+ user,
+ logger,
+ authorization,
+ } = clientArgs;
+
+ try {
+ const { value, caseVersion } = request;
+
+ decodeWithExcessOrThrow(CustomFieldPutRequestRt)(request);
+
+ const caseToUpdate = await caseService.getCase({
+ id: caseId,
+ });
+
+ if (caseToUpdate.version !== caseVersion) {
+ throw Boom.conflict(
+ `This case ${caseToUpdate.id} has been updated. Please refresh before saving additional updates.`
+ );
+ }
+
+ const configurations = await casesClient.configure.get({
+ owner: caseToUpdate.attributes.owner,
+ });
+
+ await authorization.ensureAuthorized({
+ entities: [{ owner: caseToUpdate.attributes.owner, id: caseToUpdate.id }],
+ operation: Operations.updateCase,
+ });
+
+ const foundCustomField = configurations[0]?.customFields.find(
+ (item) => item.key === customFieldId
+ );
+
+ if (!foundCustomField) {
+ throw Boom.badRequest('cannot find custom field');
+ }
+
+ validateCustomFieldTypesInRequest({
+ requestCustomFields: [
+ {
+ value,
+ type: foundCustomField.type,
+ key: customFieldId,
+ } as CaseCustomField,
+ ],
+ customFieldsConfiguration: configurations[0].customFields,
+ });
+
+ if (value == null && foundCustomField.required) {
+ throw Boom.badRequest('Custom field value cannot be null or undefined.');
+ }
+
+ const customFieldsToUpdate = [
+ {
+ value,
+ type: foundCustomField.type,
+ key: customFieldId,
+ },
+ ...caseToUpdate.attributes.customFields.filter((field) => field.key !== customFieldId),
+ ];
+
+ const decodedCustomFields =
+ decodeWithExcessOrThrow(CaseRequestCustomFieldsRt)(customFieldsToUpdate);
+
+ const updatedAt = new Date().toISOString();
+
+ const patchCasesPayload = {
+ caseId,
+ originalCase: caseToUpdate,
+ updatedAttributes: {
+ customFields: decodedCustomFields,
+ updated_at: updatedAt,
+ updated_by: user,
+ },
+ version: caseVersion,
+ };
+
+ const userActionsDict = userActionService.creator.buildUserActions({
+ updatedCases: {
+ cases: [patchCasesPayload],
+ },
+ user,
+ });
+
+ await validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 });
+
+ const updatedCase = await caseService.patchCase({
+ ...patchCasesPayload,
+ refresh: false,
+ });
+
+ const updatedCustomField = updatedCase.attributes.customFields?.find(
+ (cf) => cf.key === customFieldId
+ );
+
+ if (!updatedCustomField) {
+ throw new Error('Cannot find updated custom field.');
+ }
+
+ const builtUserActions =
+ userActionsDict != null
+ ? Object.keys(userActionsDict).reduce((acc, key) => {
+ return [...acc, ...userActionsDict[key]];
+ }, [])
+ : [];
+
+ await userActionService.creator.bulkCreateUpdateCase({
+ builtUserActions,
+ });
+
+ return decodeOrThrow(CaseCustomFieldRt)(updatedCustomField);
+ } catch (error) {
+ throw createCaseError({
+ message: `Failed to replace customField, id: ${customFieldId} of case: ${caseId} version:${request.caseVersion} : ${error}`,
+ error,
+ logger,
+ });
+ }
+};
diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts
index f9d02c094e425..c558fc258d3a2 100644
--- a/x-pack/plugins/cases/server/client/mocks.ts
+++ b/x-pack/plugins/cases/server/client/mocks.ts
@@ -59,6 +59,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => {
getReporters: jest.fn(),
getCasesByAlertID: jest.fn(),
getCategories: jest.fn(),
+ replaceCustomField: jest.fn(),
};
};
diff --git a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts
index e6c5793064545..79e5189c02f57 100644
--- a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts
+++ b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts
@@ -19,6 +19,7 @@ import { getCategoriesRoute } from './cases/categories/get_categories';
import { getCaseMetricRoute } from './internal/get_case_metrics';
import { getCasesMetricRoute } from './internal/get_cases_metrics';
import { searchCasesRoute } from './internal/search_cases';
+import { replaceCustomFieldRoute } from './internal/replace_custom_field';
export const getInternalRoutes = (userProfileService: UserProfileService) =>
[
@@ -34,4 +35,5 @@ export const getInternalRoutes = (userProfileService: UserProfileService) =>
getCaseMetricRoute,
getCasesMetricRoute,
searchCasesRoute,
+ replaceCustomFieldRoute,
] as CaseRoute[];
diff --git a/x-pack/plugins/cases/server/routes/api/internal/replace_custom_field.ts b/x-pack/plugins/cases/server/routes/api/internal/replace_custom_field.ts
new file mode 100644
index 0000000000000..c243c10064c24
--- /dev/null
+++ b/x-pack/plugins/cases/server/routes/api/internal/replace_custom_field.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+import { INTERNAL_PUT_CUSTOM_FIELDS_URL } from '../../../../common/constants';
+import { createCaseError } from '../../../common/error';
+import { createCasesRoute } from '../create_cases_route';
+import type { customFieldsApiV1 } from '../../../../common/types/api';
+import type { customFieldDomainV1 } from '../../../../common/types/domain';
+
+export const replaceCustomFieldRoute = createCasesRoute({
+ method: 'put',
+ path: INTERNAL_PUT_CUSTOM_FIELDS_URL,
+ params: {
+ params: schema.object({
+ case_id: schema.string(),
+ custom_field_id: schema.string(),
+ }),
+ },
+ handler: async ({ context, request, response }) => {
+ try {
+ const caseContext = await context.cases;
+ const casesClient = await caseContext.getCasesClient();
+ const caseId = request.params.case_id;
+ const customFieldId = request.params.custom_field_id;
+ const details = request.body as customFieldsApiV1.CustomFieldPutRequest;
+
+ const res: customFieldDomainV1.CaseCustomField = await casesClient.cases.replaceCustomField({
+ caseId,
+ customFieldId,
+ request: details,
+ });
+
+ return response.ok({
+ body: res,
+ });
+ } catch (error) {
+ throw createCaseError({
+ message: `Failed to replace customField in route: ${error}`,
+ error,
+ });
+ }
+ },
+});
diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts
index 4d7d64bbc4487..0b7f9c542a6d0 100644
--- a/x-pack/test/cases_api_integration/common/lib/api/index.ts
+++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts
@@ -37,6 +37,7 @@ import {
Case,
Cases,
CaseStatuses,
+ CaseCustomField,
} from '@kbn/cases-plugin/common/types/domain';
import {
AlertResponse,
@@ -45,6 +46,7 @@ import {
CasesFindResponse,
CasesPatchRequest,
CasesStatusResponse,
+ CustomFieldPutRequest,
GetRelatedCasesByAlertResponse,
} from '@kbn/cases-plugin/common/types/api';
import { User } from '../authentication/types';
@@ -808,3 +810,38 @@ export const searchCases = async ({
return res;
};
+
+export const replaceCustomField = async ({
+ supertest,
+ caseId,
+ customFieldId,
+ params,
+ expectedHttpCode = 200,
+ auth = { user: superUser, space: null },
+ headers = {},
+}: {
+ supertest: SuperTest.SuperTest;
+ caseId: string;
+ customFieldId: string;
+ params: CustomFieldPutRequest;
+ expectedHttpCode?: number;
+ auth?: { user: User; space: string | null } | null;
+ headers?: Record;
+}): Promise => {
+ const apiCall = supertest.put(
+ `${getSpaceUrlPrefix(
+ auth?.space
+ )}${CASES_INTERNAL_URL}/${caseId}/custom_fields/${customFieldId}`
+ );
+
+ setupAuth({ apiCall, headers, auth });
+
+ const { body: theCustomField } = await apiCall
+ .set('kbn-xsrf', 'true')
+ .set('x-elastic-internal-origin', 'foo')
+ .set(headers)
+ .send(params)
+ .expect(expectedHttpCode);
+
+ return theCustomField;
+};
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts
index 636c166169808..e731e0101bdc0 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts
@@ -56,6 +56,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./internal/user_actions_get_users'));
loadTestFile(require.resolve('./internal/bulk_delete_file_attachments'));
loadTestFile(require.resolve('./internal/search_cases'));
+ loadTestFile(require.resolve('./internal/replace_custom_field'));
/**
* Attachments framework
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/replace_custom_field.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/replace_custom_field.ts
new file mode 100644
index 0000000000000..b76079f81c62f
--- /dev/null
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/replace_custom_field.ts
@@ -0,0 +1,464 @@
+/*
+ * 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 { CustomFieldTypes } from '@kbn/cases-plugin/common/types/domain';
+
+import { FtrProviderContext } from '../../../../common/ftr_provider_context';
+import { postCaseReq, getPostCaseRequest } from '../../../../common/lib/mock';
+import {
+ deleteAllCaseItems,
+ createCase,
+ createConfiguration,
+ getConfigurationRequest,
+ replaceCustomField,
+} from '../../../../common/lib/api';
+import {
+ globalRead,
+ noKibanaPrivileges,
+ obsOnly,
+ obsOnlyRead,
+ obsSecRead,
+ secOnly,
+ secOnlyRead,
+ superUser,
+} from '../../../../common/lib/authentication/users';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+ const es = getService('es');
+
+ describe('replace_custom_field', () => {
+ afterEach(async () => {
+ await deleteAllCaseItems(es);
+ });
+
+ describe('basic tests', () => {
+ it('should replace a text customField', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'test_custom_field_2',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: 'text field value',
+ },
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ });
+ const replacedCustomField = await replaceCustomField({
+ supertest,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field_1',
+ params: {
+ value: 'this is updated text field value',
+ caseVersion: postedCase.version,
+ },
+ });
+
+ expect(replacedCustomField).to.eql({
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: 'this is updated text field value',
+ });
+ });
+
+ it('should patch a toggle customField', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'test_custom_field_2',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: 'text field value',
+ },
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ });
+ const replacedCustomField = await replaceCustomField({
+ supertest,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field_2',
+ params: {
+ value: false,
+ caseVersion: postedCase.version,
+ },
+ });
+
+ expect(replacedCustomField).to.eql({
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: false,
+ });
+ });
+
+ it('does not throw error when updating an optional custom field with a null value', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TEXT,
+ value: 'hello',
+ },
+ ],
+ });
+
+ await replaceCustomField({
+ supertest,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field',
+ params: {
+ caseVersion: postedCase.version,
+ value: null,
+ },
+ expectedHttpCode: 200,
+ });
+ });
+ });
+
+ describe('errors', () => {
+ it('400s when trying to patch with a custom field key that does not exist', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await replaceCustomField({
+ supertest,
+ caseId: postedCase.id,
+ customFieldId: 'random_key',
+ params: {
+ caseVersion: postedCase.version,
+ value: 'this is updated text field value',
+ },
+ expectedHttpCode: 400,
+ });
+ });
+
+ it('400s when trying to patch a case with a required custom field with null value', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TEXT,
+ value: 'hello',
+ },
+ ],
+ });
+
+ await replaceCustomField({
+ supertest,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field',
+ params: {
+ caseVersion: postedCase.version,
+ value: null,
+ },
+ expectedHttpCode: 400,
+ });
+ });
+
+ it('400s when trying to patch a case with a custom field with the wrong type', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await replaceCustomField({
+ supertest,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field',
+ params: {
+ caseVersion: postedCase.version,
+ value: true,
+ },
+ expectedHttpCode: 400,
+ });
+ });
+ });
+
+ describe('rbac', () => {
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+
+ it('should replace the custom field when the user has the correct permissions', async () => {
+ await createConfiguration(
+ supertestWithoutAuth,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ }),
+ 200,
+ {
+ user: secOnly,
+ space: 'space1',
+ }
+ );
+
+ const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, {
+ user: secOnly,
+ space: 'space1',
+ });
+
+ await replaceCustomField({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field',
+ params: {
+ caseVersion: postedCase.version,
+
+ value: 'this is updated text field value',
+ },
+ auth: { user: secOnly, space: 'space1' },
+ expectedHttpCode: 200,
+ });
+ });
+
+ it('should not replace a custom field when the user does not have the correct ownership', async () => {
+ await createConfiguration(
+ supertestWithoutAuth,
+ getConfigurationRequest({
+ overrides: {
+ owner: 'observabilityFixture',
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ }),
+ 200,
+ {
+ user: obsOnly,
+ space: 'space1',
+ }
+ );
+
+ const postedCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({
+ owner: 'observabilityFixture',
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TEXT,
+ value: 'hello',
+ },
+ ],
+ }),
+ 200,
+ { user: obsOnly, space: 'space1' }
+ );
+
+ await replaceCustomField({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field',
+ params: {
+ caseVersion: postedCase.version,
+ value: 'this is updated text field value',
+ },
+ auth: { user: secOnly, space: 'space1' },
+ expectedHttpCode: 403,
+ });
+ });
+
+ for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) {
+ it(`User ${
+ user.username
+ } with role(s) ${user.roles.join()} - should NOT replace a custom field`, async () => {
+ await createConfiguration(
+ supertestWithoutAuth,
+ { ...getConfigurationRequest(), owner: 'observabilityFixture' },
+ 200,
+ {
+ user: superUser,
+ space: 'space1',
+ }
+ );
+
+ const postedCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({ owner: 'securitySolutionFixture' }),
+ 200,
+ {
+ user: superUser,
+ space: 'space1',
+ }
+ );
+
+ await replaceCustomField({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field',
+ params: {
+ caseVersion: postedCase.version,
+ value: 'this is updated text field value',
+ },
+ auth: { user, space: 'space1' },
+ expectedHttpCode: 403,
+ });
+ });
+ }
+
+ it('should NOT replace a custom field in a space with no permissions', async () => {
+ await createConfiguration(
+ supertestWithoutAuth,
+ { ...getConfigurationRequest(), owner: 'observabilityFixture' },
+ 200,
+ {
+ user: superUser,
+ space: 'space2',
+ }
+ );
+
+ const postedCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({ owner: 'securitySolutionFixture' }),
+ 200,
+ {
+ user: superUser,
+ space: 'space2',
+ }
+ );
+
+ await replaceCustomField({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ customFieldId: 'test_custom_field',
+ params: {
+ caseVersion: postedCase.version,
+ value: 'this is updated text field value',
+ },
+ auth: { user: secOnly, space: 'space2' },
+ expectedHttpCode: 403,
+ });
+ });
+ });
+ });
+};
From 8709754bb2901758f5a65ad2e47b8ca97c013e0e Mon Sep 17 00:00:00 2001
From: Nick Partridge
Date: Thu, 8 Feb 2024 08:55:20 -0700
Subject: [PATCH 027/104] [Lens] Fix flaky serverless tests in group 3
(#176423)
---
.../test_suites/common/visualizations/group2/index.ts | 3 +--
.../group2/open_in_lens/agg_based/goal.ts | 11 ++++++++++-
.../group2/open_in_lens/agg_based/metric.ts | 11 ++++++++++-
.../group2/open_in_lens/agg_based/table.ts | 11 +++++++----
.../visualizations/group3/open_in_lens/tsvb/table.ts | 8 ++++----
.../visualizations/group3/open_in_lens/tsvb/top_n.ts | 7 ++++---
6 files changed, 36 insertions(+), 15 deletions(-)
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts
index 4b7e3588669d1..f3abd6dccef91 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts
@@ -10,8 +10,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ loadTestFile, getPageObject }: FtrProviderContext) => {
const svlCommonPage = getPageObject('svlCommonPage');
- // FLAKY: https://github.com/elastic/kibana/issues/168985
- describe.skip('Visualizations - Group 2', function () {
+ describe('Visualizations - Group 2', function () {
before(async () => {
await svlCommonPage.login();
});
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts
index 01f655af00a1f..b67cbe95e1ba6 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts
@@ -51,6 +51,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '140.05%',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingBar: true,
showingTrendline: false,
},
@@ -78,6 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '131,040,360.81%',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingBar: true,
showingTrendline: false,
},
@@ -106,6 +108,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '14.37%',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingBar: true,
showingTrendline: false,
},
@@ -134,6 +137,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,228,964,670.613',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingTrendline: false,
showingBar: true,
},
@@ -143,6 +147,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,186,695,551.251',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingTrendline: false,
showingBar: true,
},
@@ -152,6 +157,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,073,190,186.423',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingTrendline: false,
showingBar: true,
},
@@ -161,6 +167,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,031,579,645.108',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingTrendline: false,
showingBar: true,
},
@@ -170,6 +177,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,009,497,206.823',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingTrendline: false,
showingBar: true,
},
@@ -178,7 +186,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
subtitle: undefined,
extraText: undefined,
value: undefined,
- color: 'rgba(0, 0, 0, 0)',
+ color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingTrendline: false,
showingBar: true,
},
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts
index 9bd990484cc81..c608c454e3039 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts
@@ -47,6 +47,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '14,005',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -73,6 +74,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,104,036,080.615',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -100,6 +102,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '1,437',
color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -131,6 +134,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,228,964,670.613',
color: 'rgba(165, 0, 38, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -140,6 +144,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,186,695,551.251',
color: 'rgba(253, 191, 111, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -149,6 +154,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,073,190,186.423',
color: 'rgba(183, 224, 117, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -158,6 +164,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,031,579,645.108',
color: 'rgba(183, 224, 117, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -167,6 +174,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
extraText: '',
value: '13,009,497,206.823',
color: 'rgba(183, 224, 117, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
@@ -175,7 +183,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
subtitle: undefined,
extraText: undefined,
value: undefined,
- color: 'rgba(0, 0, 0, 0)',
+ color: 'rgba(255, 255, 255, 1)',
+ trendlineColor: undefined,
showingBar: false,
showingTrendline: false,
},
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts
index 5b5d31a842607..4761bd22d9429 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts
@@ -14,6 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const panelActions = getService('dashboardPanelActions');
const kibanaServer = getService('kibanaServer');
+ const comboBox = getService('comboBox');
describe('Table', function describeIndexTests() {
const fixture =
@@ -67,8 +68,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(await dimensions[0].getVisibleText()).to.be('Average machine.ram');
await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger');
- const summaryRowFunction = await testSubjects.find('lnsDatatable_summaryrow_function');
- expect(await summaryRowFunction.getVisibleText()).to.be('Sum');
+ expect(await comboBox.getComboBoxSelectedOptions('lnsDatatable_summaryrow_function')).to.eql([
+ 'Sum',
+ ]);
});
it('should convert sibling pipeline aggregation', async () => {
@@ -132,8 +134,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const percentageColumnText = await lens.getDimensionTriggerText('lnsDatatable_metrics', 1);
await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger', 0, 1);
- const format = await testSubjects.find('indexPattern-dimension-format');
- expect(await format.getVisibleText()).to.be('Percent');
+ expect(await comboBox.getComboBoxSelectedOptions('indexPattern-dimension-format')).to.eql([
+ 'Percent',
+ ]);
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(dimensions).to.have.length(2);
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts
index 864e205e3ff5f..e5b4174435e05 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts
@@ -22,7 +22,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const retry = getService('retry');
const panelActions = getService('dashboardPanelActions');
const kibanaServer = getService('kibanaServer');
- const comboBox = getService('comboBox');
describe('Table', function describeIndexTests() {
const fixture =
@@ -84,9 +83,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger');
await testSubjects.click('indexPattern-advanced-accordion');
- expect(
- await comboBox.getComboBoxSelectedOptions('indexPattern-dimension-reducedTimeRange')
- ).to.eql(['1 minute (1m)']);
+ const reducedTimeRange = await testSubjects.find(
+ 'indexPattern-dimension-reducedTimeRange > comboBoxSearchInput'
+ );
+ expect(await reducedTimeRange.getAttribute('value')).to.be('1 minute (1m)');
await retry.try(async () => {
const layerCount = await lens.getLayerCount();
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts
index 4085e7faad08c..e9872a6b776d3 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts
@@ -17,7 +17,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const queryBar = getService('queryBar');
const panelActions = getService('dashboardPanelActions');
const kibanaServer = getService('kibanaServer');
- const comboBox = getService('comboBox');
describe('Top N', function describeIndexTests() {
const fixture =
@@ -102,8 +101,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger');
await testSubjects.click('indexPattern-advanced-accordion');
- const reducedTimeRange = await testSubjects.find('indexPattern-dimension-reducedTimeRange');
- await comboBox.isOptionSelected(reducedTimeRange, '1 minute (1m)');
+ const reducedTimeRange = await testSubjects.find(
+ 'indexPattern-dimension-reducedTimeRange > comboBoxSearchInput'
+ );
+ expect(await reducedTimeRange.getAttribute('value')).to.be('1 minute (1m)');
await retry.try(async () => {
const layerCount = await lens.getLayerCount();
expect(layerCount).to.be(1);
From 3e4f1ed99dcaadd6539f198ddf4623a8e2a723bb Mon Sep 17 00:00:00 2001
From: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com>
Date: Thu, 8 Feb 2024 16:02:54 +0000
Subject: [PATCH 028/104] [ILM] Fix data allocation field (#176292)
Fixes https://github.com/elastic/kibana/issues/173764
## Summary
This PR fixes the "Custom" data allocation field in the edit IL policy
form by adding a default value for the field. Before, there was no
default value, so no value would be added to the request until the user
changes the initial selection to another option. This explains the bug
described from https://github.com/elastic/kibana/issues/173764 (in
v7.17) where there is only one option and the user is not able to change
it to something else so the value of the only option cannot be added to
the request. Now with these changes, if the user doesn't make any
changes to the initially selected option, the default value would be
added to the request.
**How to test:**
1. Start Es with `yarn es snapshot` and Kibana with `yarn start`.
2. Go to Stack Management -> Index Lifecycle Policies and start creating
a new policy.
3. Type in some name for the policy. Enable the Warm (or Cold) phase and
type in some number in the "Move data into phase when" field. Click on
"Advanced settings".
4. In the "Data allocation" field, select the "Custom" option.
5. Click on "Show request" and verify that the request correctly shows
the `allocation` field (the "Any data node" option doesn't add anything
to the request).
6. Select another option for the node attribution field and verify it is
displayed in the request.
Test the field on cloud by accessing the [CI cloud
deployment](https://kibana-pr-176292.kb.us-west2.gcp.elastic-cloud.com:9243/)
from this PR or by changing the if-condition in line 65 in
`x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx`
to `if (false)`. Now the "Any data node" option is not available. Follow
the test instructions above and verify that the initially selected node
attribute option is added to the request.
---
.../data_tier_allocation_field/components/node_allocation.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
index a2bad060a5893..9de959e4d5e83 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
@@ -100,6 +100,9 @@ export const NodeAllocation: FunctionComponent = ({
Date: Thu, 8 Feb 2024 16:13:34 +0000
Subject: [PATCH 029/104] Update dependency @elastic/charts to v63 (main)
(#175316)
## Note about `@elastic/charts` BREAKING CHANGE
In version 62.0.0 we introduced a breaking change in time-series charts:
the default "extra" legend value now represents the last data point in
the passed data array. It doesn't try to reconcile anymore the data
computed domain with a passed domain in `Settings.xDomain` but instead
it renders directly the last element of the passed array.
The reasons for this change can be found at
https://github.com/elastic/elastic-charts/pull/2115 or can be asked
directly to our `#charts` slack channel
There are a couple of implementations in Kibana that use both the
`showLegendExtra` in the chart configuration. I've commented them out so
that the owner teams can help me fix this breaking change if necessary.
In general, the fix requires that the data passed to the chart contains
all the buckets, even empty buckets with null/zeros should be passed. To
achieve this, your ES query you should provide the `extended_bounds`
settings in the [data histogram
agg](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html#search-aggregations-bucket-histogram-aggregation-extended-bounds)
and use a `min_doc_count:0`. If that doesn't work, please ping me and we
can find an alternative solution.
This should not limit the query performance, generating empty date
buckets on the server side has a similar or even less performance impact
than what we were doing on the client side to calculate every missing
bucket, to fillup the chart in particular situations.
Please double-check your queries/data fetches and push a commit to this
PR or ping me with the updated data fetch strategy.
fix https://github.com/elastic/kibana/issues/153079
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@elastic/charts](https://togithub.com/elastic/elastic-charts) |
[`61.2.0` ->
`63.0.0`](https://renovatebot.com/diffs/npm/@elastic%2fcharts/61.2.0/63.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@elastic%2fcharts/63.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@elastic%2fcharts/63.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@elastic%2fcharts/61.2.0/63.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@elastic%2fcharts/61.2.0/63.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
---
### Release Notes
elastic/elastic-charts (@elastic/charts)
###
[`v63.0.0`](https://togithub.com/elastic/elastic-charts/blob/HEAD/CHANGELOG.md#6300-2024-01-24)
[Compare
Source](https://togithub.com/elastic/elastic-charts/compare/v62.0.0...v63.0.0)
##### Features
- **legend:** expose extra raw values
([#2308](https://togithub.com/elastic/elastic-charts/issues/2308))
([85bfe06](https://togithub.com/elastic/elastic-charts/commit/85bfe0668d66fd24e78f2bba8be4570fa926e94c))
##### BREAKING CHANGES
- **legend:** The `CustomLegend.item` now exposes both the `raw` and the
`formatted` version of the extra value.
###
[`v62.0.0`](https://togithub.com/elastic/elastic-charts/blob/HEAD/CHANGELOG.md#6200-2024-01-23)
[Compare
Source](https://togithub.com/elastic/elastic-charts/compare/v61.2.0...v62.0.0)
##### Bug Fixes
- **deps:** update dependency
[@elastic/eui](https://togithub.com/elastic/eui) to ^91.3.1
([#2286](https://togithub.com/elastic/elastic-charts/issues/2286))
([d4d7b5d](https://togithub.com/elastic/elastic-charts/commit/d4d7b5db6681ec0c65ef8b7e576f1b5fc8b5433a))
- **deps:** update dependency
[@elastic/eui](https://togithub.com/elastic/eui) to v92
([#2290](https://togithub.com/elastic/elastic-charts/issues/2290))
([cc537fa](https://togithub.com/elastic/elastic-charts/commit/cc537faf43d88acc9abab7e0dac9360bd460b574))
- **legend:** improve last value handling
([#2115](https://togithub.com/elastic/elastic-charts/issues/2115))
([9f99447](https://togithub.com/elastic/elastic-charts/commit/9f9944734c4a13bfe9e4ffc9f4c0f39da5f9931f))
##### BREAKING CHANGES
- **legend:** In cartesian charts, the default legend value now
represents the data points that coincide with the latest datum in the X
domain. Please consider passing every data point, even the empty ones
(like empty buckets/bins/etc) if your x data domain doesn't fully cover
a custom x domain passed to the chart configuration.
---
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Marco Vettorello
Co-authored-by: Stratoula Kalafateli
Co-authored-by: Elena Stoeva
---
package.json | 2 +-
.../overview/components/sections/logs/logs_section.tsx | 6 +++++-
.../common/__snapshots__/external_alert.test.ts.snap | 4 ++++
.../alerts/__snapshots__/alerts_histogram.test.ts.snap | 8 ++++++++
.../lens_attributes/common/alerts/alerts_histogram.ts | 2 ++
.../lens_attributes/common/events.ts | 2 ++
.../lens_attributes/common/external_alert.ts | 2 ++
.../network/__snapshots__/dns_top_domains.test.ts.snap | 4 ++++
.../lens_attributes/network/dns_top_domains.ts | 2 ++
.../public/rule_types/threshold/visualization.tsx | 6 +++++-
.../components/common/charts/duration_chart.tsx | 6 +++++-
.../models/watch/threshold_watch/build_visualize_query.js | 6 +++++-
yarn.lock | 8 ++++----
13 files changed, 49 insertions(+), 9 deletions(-)
diff --git a/package.json b/package.json
index ccd5182e04a55..4676515b4f889 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
"@dnd-kit/utilities": "^2.0.0",
"@elastic/apm-rum": "^5.16.0",
"@elastic/apm-rum-react": "^2.0.2",
- "@elastic/charts": "61.2.0",
+ "@elastic/charts": "63.0.0",
"@elastic/datemath": "5.0.3",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.9.1-canary.1",
"@elastic/ems-client": "8.5.1",
diff --git a/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx b/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx
index 63087aad6dd30..2651cef2191d0 100644
--- a/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx
+++ b/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx
@@ -142,7 +142,11 @@ export function LogsSection({ bucketSize }: Props) {
showLegend
legendPosition={Position.Right}
xDomain={{ min, max }}
- showLegendExtra
+ // Please double check if the data passed to the chart contains all the buckets, even the empty ones.
+ // the showLegendExtra will display the last element of the data array as the default legend value
+ // and if empty buckets are filtered out you can probably see a value that doesn't correspond
+ // to the value in the last time bucket visualized.
+ // showLegendExtra
locale={i18n.getLocale()}
/>
{series &&
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap
index e8bdc77c7c460..4e26d269ed8ad 100644
--- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap
+++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap
@@ -41,6 +41,9 @@ Object {
"isBucketed": false,
"label": "Count of records",
"operationType": "count",
+ "params": Object {
+ "emptyAsNull": true,
+ },
"scale": "ratio",
"sourceField": "___records___",
},
@@ -50,6 +53,7 @@ Object {
"label": "@timestamp",
"operationType": "date_histogram",
"params": Object {
+ "includeEmptyRows": true,
"interval": "auto",
},
"scale": "interval",
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap
index a48df2b2787e6..5e666cc63c3e9 100644
--- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap
+++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap
@@ -50,6 +50,7 @@ Object {
"label": "@timestamp",
"operationType": "date_histogram",
"params": Object {
+ "includeEmptyRows": true,
"interval": "auto",
},
"scale": "interval",
@@ -60,6 +61,9 @@ Object {
"isBucketed": false,
"label": "Count of records",
"operationType": "count",
+ "params": Object {
+ "emptyAsNull": true,
+ },
"scale": "ratio",
"sourceField": "___records___",
},
@@ -233,6 +237,7 @@ Object {
"label": "@timestamp",
"operationType": "date_histogram",
"params": Object {
+ "includeEmptyRows": true,
"interval": "auto",
},
"scale": "interval",
@@ -243,6 +248,9 @@ Object {
"isBucketed": false,
"label": "Count of records",
"operationType": "count",
+ "params": Object {
+ "emptyAsNull": true,
+ },
"scale": "ratio",
"sourceField": "___records___",
},
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts
index 571040b33b378..5fb0630bcca80 100644
--- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts
+++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts
@@ -70,6 +70,7 @@ export const getAlertsHistogramLensAttributes: GetLensAttributes = (
scale: 'interval',
params: {
interval: 'auto',
+ includeEmptyRows: true,
},
},
'e09e0380-0740-4105-becc-0a4ca12e3944': {
@@ -79,6 +80,7 @@ export const getAlertsHistogramLensAttributes: GetLensAttributes = (
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
+ params: { emptyAsNull: true },
},
'34919782-4546-43a5-b668-06ac934d3acd': {
label: `Top values of ${stackByField}`,
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts
index baa267b9f17b8..8e238ca11b1d7 100644
--- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts
+++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts
@@ -71,6 +71,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = (
scale: 'interval',
params: {
interval: 'auto',
+ includeEmptyRows: true,
},
},
'e09e0380-0740-4105-becc-0a4ca12e3944': {
@@ -80,6 +81,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = (
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
+ params: { emptyAsNull: true },
},
'34919782-4546-43a5-b668-06ac934d3acd': {
label: `Top values of ${stackByField}`,
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts
index 2aa3eab25d105..3baec52d3b8fb 100644
--- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts
+++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts
@@ -91,6 +91,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = (
scale: 'interval',
params: {
interval: 'auto',
+ includeEmptyRows: true,
},
},
'0a923af2-c880-4aa3-aa93-a0b9c2801f6d': {
@@ -100,6 +101,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = (
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
+ params: { emptyAsNull: true },
},
'42334c6e-98d9-47a2-b4cb-a445abb44c93': {
label: TOP_VALUE(`${stackByField}`), // could be event.category
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap
index 5d7a36ab09e20..19a5eb4f45b71 100644
--- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap
+++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap
@@ -26,6 +26,9 @@ Object {
"isBucketed": false,
"label": "Unique count of dns.question.name",
"operationType": "unique_count",
+ "params": Object {
+ "emptyAsNull": true,
+ },
"scale": "ratio",
"sourceField": "dns.question.name",
},
@@ -35,6 +38,7 @@ Object {
"label": "@timestamp",
"operationType": "date_histogram",
"params": Object {
+ "includeEmptyRows": true,
"interval": "auto",
},
"scale": "interval",
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts
index f39ad3c2ea30d..39f5d56a3d461 100644
--- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts
+++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts
@@ -133,6 +133,7 @@ export const getDnsTopDomainsLensAttributes: GetLensAttributes = (
scale: 'interval',
params: {
interval: 'auto',
+ includeEmptyRows: true,
},
},
'2a4d5e20-f570-48e4-b9ab-ff3068919377': {
@@ -142,6 +143,7 @@ export const getDnsTopDomainsLensAttributes: GetLensAttributes = (
scale: 'ratio',
sourceField: 'dns.question.name',
isBucketed: false,
+ params: { emptyAsNull: true },
},
},
columnOrder: [
diff --git a/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx
index c8996d9fcaa4b..918c87c5d13a8 100644
--- a/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx
+++ b/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx
@@ -269,7 +269,11 @@ export const ThresholdVisualization: React.FunctionComponent = ({
baseTheme={chartsBaseTheme}
xDomain={domain}
showLegend={!!termField}
- showLegendExtra
+ // Please double check if the data passed to the chart contains all the buckets, even the empty ones.
+ // the showLegendExtra will display the last element of the data array as the default legend value
+ // and if empty buckets are filtered out you can probably see a value that doesn't correspond
+ // to the value in the last time bucket visualized.
+ // showLegendExtra
legendPosition={Position.Bottom}
locale={i18n.getLocale()}
/>
diff --git a/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx
index 8e5da4fa970ab..d85654b195c6d 100644
--- a/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx
+++ b/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx
@@ -106,7 +106,11 @@ export const DurationChartComponent = ({
Date: Thu, 8 Feb 2024 17:15:42 +0100
Subject: [PATCH 030/104] Sort printed mappings and fields in update check CLI
(#176493)
## Summary
Close https://github.com/elastic/kibana/issues/168927
Adds a new utility to pretty print and sort JS objects and use this in
the mappings update check CLI for both fields and mappings.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../current_fields.json | 1072 ++---
.../current_mappings.json | 4166 ++++++++---------
.../src/compatibility/current_mappings.ts | 13 +-
.../run_mappings_compatibility_check.ts | 4 +-
.../src/mappings_additions/current_fields.ts | 3 +-
.../tsconfig.json | 1 +
packages/kbn-utils/index.ts | 2 +-
packages/kbn-utils/src/json/index.test.ts | 85 +
packages/kbn-utils/src/json/index.ts | 20 +
9 files changed, 2739 insertions(+), 2627 deletions(-)
create mode 100644 packages/kbn-utils/src/json/index.test.ts
create mode 100644 packages/kbn-utils/src/json/index.ts
diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json
index 56308a980cc56..4d8c775710f09 100644
--- a/packages/kbn-check-mappings-update-cli/current_fields.json
+++ b/packages/kbn-check-mappings-update-cli/current_fields.json
@@ -1,198 +1,9 @@
{
- "core-usage-stats": [],
- "legacy-url-alias": [
- "disabled",
- "resolveCounter",
- "sourceId",
- "targetId",
- "targetNamespace",
- "targetType"
- ],
- "config": [
- "buildNum"
- ],
- "config-global": [
- "buildNum"
- ],
- "url": [
- "accessDate",
- "createDate",
- "slug"
- ],
- "usage-counters": [
- "domainId"
- ],
- "task": [
- "attempts",
- "enabled",
- "ownerId",
- "retryAt",
- "runAt",
- "schedule",
- "schedule.interval",
- "scheduledAt",
- "scope",
- "status",
- "taskType"
- ],
- "guided-onboarding-guide-state": [
- "guideId",
- "isActive"
- ],
- "guided-onboarding-plugin-state": [],
- "ui-metric": [
- "count"
- ],
- "application_usage_totals": [],
- "application_usage_daily": [
- "timestamp"
- ],
- "event_loop_delays_daily": [
- "lastUpdatedAt"
- ],
- "index-pattern": [
- "name",
- "title",
- "type"
- ],
- "sample-data-telemetry": [
- "installCount",
- "unInstallCount"
- ],
- "space": [
- "name"
- ],
- "spaces-usage-stats": [],
- "exception-list-agnostic": [
- "_tags",
- "comments",
- "comments.comment",
- "comments.created_at",
- "comments.created_by",
- "comments.id",
- "comments.updated_at",
- "comments.updated_by",
- "created_at",
- "created_by",
- "description",
- "entries",
- "entries.entries",
- "entries.entries.field",
- "entries.entries.operator",
- "entries.entries.type",
- "entries.entries.value",
- "entries.field",
- "entries.list",
- "entries.list.id",
- "entries.list.type",
- "entries.operator",
- "entries.type",
- "entries.value",
- "expire_time",
- "immutable",
- "item_id",
- "list_id",
- "list_type",
- "meta",
- "name",
- "os_types",
- "tags",
- "tie_breaker_id",
- "type",
- "updated_by",
- "version"
- ],
- "exception-list": [
- "_tags",
- "comments",
- "comments.comment",
- "comments.created_at",
- "comments.created_by",
- "comments.id",
- "comments.updated_at",
- "comments.updated_by",
- "created_at",
- "created_by",
- "description",
- "entries",
- "entries.entries",
- "entries.entries.field",
- "entries.entries.operator",
- "entries.entries.type",
- "entries.entries.value",
- "entries.field",
- "entries.list",
- "entries.list.id",
- "entries.list.type",
- "entries.operator",
- "entries.type",
- "entries.value",
- "expire_time",
- "immutable",
- "item_id",
- "list_id",
- "list_type",
- "meta",
- "name",
- "os_types",
- "tags",
- "tie_breaker_id",
- "type",
- "updated_by",
- "version"
- ],
- "telemetry": [],
- "file": [
- "FileKind",
- "Meta",
- "Status",
- "Updated",
- "created",
- "extension",
- "hash",
- "mime_type",
- "name",
- "size",
- "user"
- ],
- "fileShare": [
- "created",
- "name",
- "token",
- "valid_until"
- ],
"action": [
"actionTypeId",
"name"
],
"action_task_params": [],
- "connector_token": [
- "connectorId",
- "tokenType"
- ],
- "query": [
- "description",
- "title"
- ],
- "kql-telemetry": [],
- "search-session": [
- "created",
- "realmName",
- "realmType",
- "sessionId",
- "username"
- ],
- "search-telemetry": [],
- "file-upload-usage-collection-telemetry": [
- "file_upload",
- "file_upload.index_creation_count"
- ],
- "apm-indices": [],
- "tag": [
- "color",
- "description",
- "name"
- ],
"alert": [
"actions",
"actions.actionRef",
@@ -265,34 +76,28 @@
"apiKeyId",
"createdAt"
],
- "rules-settings": [
- "flapping"
- ],
- "maintenance-window": [
- "enabled",
- "events"
+ "apm-custom-dashboards": [
+ "dashboardSavedObjectId",
+ "kuery",
+ "serviceEnvironmentFilterEnabled",
+ "serviceNameFilterEnabled"
],
- "graph-workspace": [
- "description",
- "kibanaSavedObjectMeta",
- "kibanaSavedObjectMeta.searchSourceJSON",
- "legacyIndexPatternRef",
- "numLinks",
- "numVertices",
- "title",
- "version",
- "wsState"
+ "apm-indices": [],
+ "apm-server-schema": [
+ "schemaJson"
],
- "search": [
+ "apm-service-group": [
+ "color",
"description",
- "title"
+ "groupName",
+ "kuery"
],
- "visualization": [
- "description",
- "kibanaSavedObjectMeta",
- "title",
- "version"
+ "apm-telemetry": [],
+ "app_search_telemetry": [],
+ "application_usage_daily": [
+ "timestamp"
],
+ "application_usage_totals": [],
"canvas-element": [
"@created",
"@timestamp",
@@ -312,9 +117,128 @@
"tags",
"template_key"
],
- "event-annotation-group": [
+ "cases": [
+ "assignees",
+ "assignees.uid",
+ "category",
+ "closed_at",
+ "closed_by",
+ "closed_by.email",
+ "closed_by.full_name",
+ "closed_by.profile_uid",
+ "closed_by.username",
+ "connector",
+ "connector.fields",
+ "connector.fields.key",
+ "connector.fields.value",
+ "connector.name",
+ "connector.type",
+ "created_at",
+ "created_by",
+ "created_by.email",
+ "created_by.full_name",
+ "created_by.profile_uid",
+ "created_by.username",
+ "customFields",
+ "customFields.key",
+ "customFields.type",
+ "customFields.value",
"description",
- "title"
+ "duration",
+ "external_service",
+ "external_service.connector_name",
+ "external_service.external_id",
+ "external_service.external_title",
+ "external_service.external_url",
+ "external_service.pushed_at",
+ "external_service.pushed_by",
+ "external_service.pushed_by.email",
+ "external_service.pushed_by.full_name",
+ "external_service.pushed_by.profile_uid",
+ "external_service.pushed_by.username",
+ "owner",
+ "settings",
+ "settings.syncAlerts",
+ "severity",
+ "status",
+ "tags",
+ "title",
+ "total_alerts",
+ "total_comments",
+ "updated_at",
+ "updated_by",
+ "updated_by.email",
+ "updated_by.full_name",
+ "updated_by.profile_uid",
+ "updated_by.username"
+ ],
+ "cases-comments": [
+ "actions",
+ "actions.type",
+ "alertId",
+ "comment",
+ "created_at",
+ "created_by",
+ "created_by.username",
+ "externalReferenceAttachmentTypeId",
+ "owner",
+ "persistableStateAttachmentTypeId",
+ "pushed_at",
+ "type",
+ "updated_at"
+ ],
+ "cases-configure": [
+ "closure_type",
+ "created_at",
+ "owner"
+ ],
+ "cases-connector-mappings": [
+ "owner"
+ ],
+ "cases-telemetry": [],
+ "cases-user-actions": [
+ "action",
+ "created_at",
+ "created_by",
+ "created_by.username",
+ "owner",
+ "payload",
+ "payload.assignees",
+ "payload.assignees.uid",
+ "payload.comment",
+ "payload.comment.externalReferenceAttachmentTypeId",
+ "payload.comment.persistableStateAttachmentTypeId",
+ "payload.comment.type",
+ "payload.connector",
+ "payload.connector.type",
+ "type"
+ ],
+ "cloud-security-posture-settings": [
+ "rules"
+ ],
+ "config": [
+ "buildNum"
+ ],
+ "config-global": [
+ "buildNum"
+ ],
+ "connector_token": [
+ "connectorId",
+ "tokenType"
+ ],
+ "core-usage-stats": [],
+ "csp-rule-template": [
+ "metadata",
+ "metadata.benchmark",
+ "metadata.benchmark.id",
+ "metadata.benchmark.name",
+ "metadata.benchmark.posture_type",
+ "metadata.benchmark.rule_number",
+ "metadata.benchmark.version",
+ "metadata.id",
+ "metadata.name",
+ "metadata.section",
+ "metadata.version"
],
"dashboard": [
"controlGroupInput",
@@ -339,139 +263,206 @@
"title",
"version"
],
- "links": [
- "description",
- "links",
- "title"
+ "endpoint:user-artifact-manifest": [
+ "artifacts",
+ "schemaVersion"
],
- "lens": [
+ "enterprise_search_telemetry": [],
+ "epm-packages": [
+ "es_index_patterns",
+ "experimental_data_stream_features",
+ "experimental_data_stream_features.data_stream",
+ "experimental_data_stream_features.features",
+ "experimental_data_stream_features.features.synthetic_source",
+ "experimental_data_stream_features.features.tsdb",
+ "install_format_schema_version",
+ "install_source",
+ "install_started_at",
+ "install_status",
+ "install_version",
+ "installed_es",
+ "installed_es.deferred",
+ "installed_es.id",
+ "installed_es.type",
+ "installed_es.version",
+ "installed_kibana",
+ "installed_kibana_space_id",
+ "internal",
+ "keep_policies_up_to_date",
+ "latest_install_failed_attempts",
+ "name",
+ "package_assets",
+ "verification_key_id",
+ "verification_status",
+ "version"
+ ],
+ "epm-packages-assets": [
+ "asset_path",
+ "data_base64",
+ "data_utf8",
+ "install_source",
+ "media_type",
+ "package_name",
+ "package_version"
+ ],
+ "event-annotation-group": [
"description",
- "state",
- "title",
- "visualizationType"
+ "title"
],
- "lens-ui-telemetry": [
- "count",
- "date",
- "name",
- "type"
+ "event_loop_delays_daily": [
+ "lastUpdatedAt"
],
- "map": [
- "bounds",
+ "exception-list": [
+ "_tags",
+ "comments",
+ "comments.comment",
+ "comments.created_at",
+ "comments.created_by",
+ "comments.id",
+ "comments.updated_at",
+ "comments.updated_by",
+ "created_at",
+ "created_by",
"description",
- "layerListJSON",
- "mapStateJSON",
- "title",
- "uiStateJSON",
+ "entries",
+ "entries.entries",
+ "entries.entries.field",
+ "entries.entries.operator",
+ "entries.entries.type",
+ "entries.entries.value",
+ "entries.field",
+ "entries.list",
+ "entries.list.id",
+ "entries.list.type",
+ "entries.operator",
+ "entries.type",
+ "entries.value",
+ "expire_time",
+ "immutable",
+ "item_id",
+ "list_id",
+ "list_type",
+ "meta",
+ "name",
+ "os_types",
+ "tags",
+ "tie_breaker_id",
+ "type",
+ "updated_by",
"version"
],
- "cases-comments": [
- "actions",
- "actions.type",
- "alertId",
- "comment",
+ "exception-list-agnostic": [
+ "_tags",
+ "comments",
+ "comments.comment",
+ "comments.created_at",
+ "comments.created_by",
+ "comments.id",
+ "comments.updated_at",
+ "comments.updated_by",
"created_at",
"created_by",
- "created_by.username",
- "externalReferenceAttachmentTypeId",
- "owner",
- "persistableStateAttachmentTypeId",
- "pushed_at",
+ "description",
+ "entries",
+ "entries.entries",
+ "entries.entries.field",
+ "entries.entries.operator",
+ "entries.entries.type",
+ "entries.entries.value",
+ "entries.field",
+ "entries.list",
+ "entries.list.id",
+ "entries.list.type",
+ "entries.operator",
+ "entries.type",
+ "entries.value",
+ "expire_time",
+ "immutable",
+ "item_id",
+ "list_id",
+ "list_type",
+ "meta",
+ "name",
+ "os_types",
+ "tags",
+ "tie_breaker_id",
"type",
- "updated_at"
+ "updated_by",
+ "version"
],
- "cases-configure": [
- "closure_type",
- "created_at",
- "owner"
+ "file": [
+ "FileKind",
+ "Meta",
+ "Status",
+ "Updated",
+ "created",
+ "extension",
+ "hash",
+ "mime_type",
+ "name",
+ "size",
+ "user"
],
- "cases-connector-mappings": [
- "owner"
+ "file-upload-usage-collection-telemetry": [
+ "file_upload",
+ "file_upload.index_creation_count"
],
- "cases": [
- "assignees",
- "assignees.uid",
- "category",
- "closed_at",
- "closed_by",
- "closed_by.email",
- "closed_by.full_name",
- "closed_by.profile_uid",
- "closed_by.username",
- "connector",
- "connector.fields",
- "connector.fields.key",
- "connector.fields.value",
- "connector.name",
- "connector.type",
- "created_at",
- "created_by",
- "created_by.email",
- "created_by.full_name",
- "created_by.profile_uid",
- "created_by.username",
- "customFields",
- "customFields.key",
- "customFields.type",
- "customFields.value",
+ "fileShare": [
+ "created",
+ "name",
+ "token",
+ "valid_until"
+ ],
+ "fleet-fleet-server-host": [
+ "host_urls",
+ "is_default",
+ "is_internal",
+ "is_preconfigured",
+ "name",
+ "proxy_id"
+ ],
+ "fleet-message-signing-keys": [],
+ "fleet-preconfiguration-deletion-record": [
+ "id"
+ ],
+ "fleet-proxy": [
+ "certificate",
+ "certificate_authorities",
+ "certificate_key",
+ "is_preconfigured",
+ "name",
+ "proxy_headers",
+ "url"
+ ],
+ "fleet-uninstall-tokens": [
+ "policy_id",
+ "token_plain"
+ ],
+ "graph-workspace": [
"description",
- "duration",
- "external_service",
- "external_service.connector_name",
- "external_service.external_id",
- "external_service.external_title",
- "external_service.external_url",
- "external_service.pushed_at",
- "external_service.pushed_by",
- "external_service.pushed_by.email",
- "external_service.pushed_by.full_name",
- "external_service.pushed_by.profile_uid",
- "external_service.pushed_by.username",
- "owner",
- "settings",
- "settings.syncAlerts",
- "severity",
- "status",
- "tags",
+ "kibanaSavedObjectMeta",
+ "kibanaSavedObjectMeta.searchSourceJSON",
+ "legacyIndexPatternRef",
+ "numLinks",
+ "numVertices",
"title",
- "total_alerts",
- "total_comments",
- "updated_at",
- "updated_by",
- "updated_by.email",
- "updated_by.full_name",
- "updated_by.profile_uid",
- "updated_by.username"
+ "version",
+ "wsState"
],
- "cases-user-actions": [
- "action",
- "created_at",
- "created_by",
- "created_by.username",
- "owner",
- "payload",
- "payload.assignees",
- "payload.assignees.uid",
- "payload.comment",
- "payload.comment.externalReferenceAttachmentTypeId",
- "payload.comment.persistableStateAttachmentTypeId",
- "payload.comment.type",
- "payload.connector",
- "payload.connector.type",
+ "guided-onboarding-guide-state": [
+ "guideId",
+ "isActive"
+ ],
+ "guided-onboarding-plugin-state": [],
+ "index-pattern": [
+ "name",
+ "title",
"type"
],
- "cases-telemetry": [],
"infrastructure-monitoring-log-view": [
"name"
],
- "metrics-data-source": [],
- "ingest_manager_settings": [
- "fleet_server_hosts",
- "has_seen_add_data_notice",
- "output_secret_storage_requirements_met",
- "prerelease_integrations_enabled",
- "secret_storage_requirements_met"
- ],
+ "infrastructure-ui-source": [],
"ingest-agent-policies": [
"agent_features",
"agent_features.enabled",
@@ -499,6 +490,13 @@
"updated_at",
"updated_by"
],
+ "ingest-download-sources": [
+ "host",
+ "is_default",
+ "name",
+ "proxy_id",
+ "source_id"
+ ],
"ingest-outputs": [
"allow_edit",
"auth_type",
@@ -582,92 +580,89 @@
"updated_by",
"vars"
],
- "epm-packages": [
- "es_index_patterns",
- "experimental_data_stream_features",
- "experimental_data_stream_features.data_stream",
- "experimental_data_stream_features.features",
- "experimental_data_stream_features.features.synthetic_source",
- "experimental_data_stream_features.features.tsdb",
- "install_format_schema_version",
- "install_source",
- "install_started_at",
- "install_status",
- "install_version",
- "installed_es",
- "installed_es.deferred",
- "installed_es.id",
- "installed_es.type",
- "installed_es.version",
- "installed_kibana",
- "installed_kibana_space_id",
- "internal",
- "keep_policies_up_to_date",
- "latest_install_failed_attempts",
- "name",
- "package_assets",
- "verification_key_id",
- "verification_status",
- "version"
+ "ingest_manager_settings": [
+ "fleet_server_hosts",
+ "has_seen_add_data_notice",
+ "output_secret_storage_requirements_met",
+ "prerelease_integrations_enabled",
+ "secret_storage_requirements_met"
],
- "epm-packages-assets": [
- "asset_path",
- "data_base64",
- "data_utf8",
- "install_source",
- "media_type",
- "package_name",
- "package_version"
+ "inventory-view": [],
+ "kql-telemetry": [],
+ "legacy-url-alias": [
+ "disabled",
+ "resolveCounter",
+ "sourceId",
+ "targetId",
+ "targetNamespace",
+ "targetType"
],
- "fleet-preconfiguration-deletion-record": [
- "id"
+ "lens": [
+ "description",
+ "state",
+ "title",
+ "visualizationType"
],
- "ingest-download-sources": [
- "host",
- "is_default",
+ "lens-ui-telemetry": [
+ "count",
+ "date",
"name",
- "proxy_id",
- "source_id"
+ "type"
],
- "fleet-fleet-server-host": [
- "host_urls",
- "is_default",
- "is_internal",
- "is_preconfigured",
- "name",
- "proxy_id"
+ "links": [
+ "description",
+ "links",
+ "title"
],
- "fleet-proxy": [
- "certificate",
- "certificate_authorities",
- "certificate_key",
- "is_preconfigured",
- "name",
- "proxy_headers",
- "url"
+ "maintenance-window": [
+ "enabled",
+ "events"
],
- "fleet-message-signing-keys": [],
- "fleet-uninstall-tokens": [
- "policy_id",
- "token_plain"
+ "map": [
+ "bounds",
+ "description",
+ "layerListJSON",
+ "mapStateJSON",
+ "title",
+ "uiStateJSON",
+ "version"
],
- "osquery-manager-usage-metric": [
- "count",
- "errors"
+ "metrics-data-source": [],
+ "metrics-explorer-view": [],
+ "ml-job": [
+ "datafeed_id",
+ "job_id",
+ "type"
],
- "osquery-saved-query": [
- "created_at",
- "created_by",
+ "ml-module": [
+ "datafeeds",
+ "defaultIndexPattern",
"description",
- "ecs_mapping",
"id",
- "interval",
- "platform",
+ "jobs",
+ "logo",
"query",
- "timeout",
- "updated_at",
- "updated_by",
- "version"
+ "tags",
+ "title",
+ "type"
+ ],
+ "ml-trained-model": [
+ "job",
+ "job.create_time",
+ "job.job_id",
+ "model_id"
+ ],
+ "monitoring-telemetry": [
+ "reportedClusterUuids"
+ ],
+ "observability-onboarding-state": [
+ "progress",
+ "state",
+ "type"
+ ],
+ "osquery-manager-usage-metric": [
+ "count",
+ "errors"
],
"osquery-pack": [
"created_at",
@@ -702,122 +697,65 @@
"shards",
"version"
],
- "csp-rule-template": [
- "metadata",
- "metadata.benchmark",
- "metadata.benchmark.id",
- "metadata.benchmark.name",
- "metadata.benchmark.posture_type",
- "metadata.benchmark.rule_number",
- "metadata.benchmark.version",
- "metadata.id",
- "metadata.name",
- "metadata.section",
- "metadata.version"
- ],
- "slo": [
- "budgetingMethod",
+ "osquery-saved-query": [
+ "created_at",
+ "created_by",
"description",
- "enabled",
+ "ecs_mapping",
"id",
- "indicator",
- "indicator.params",
- "indicator.type",
- "name",
- "tags",
+ "interval",
+ "platform",
+ "query",
+ "timeout",
+ "updated_at",
+ "updated_by",
"version"
],
- "threshold-explorer-view": [],
- "observability-onboarding-state": [
- "progress",
- "state",
- "type"
- ],
- "ml-job": [
- "datafeed_id",
- "job_id",
- "type"
- ],
- "ml-trained-model": [
- "job",
- "job.create_time",
- "job.job_id",
- "model_id"
+ "policy-settings-protection-updates-note": [
+ "note"
],
- "ml-module": [
- "datafeeds",
- "defaultIndexPattern",
+ "query": [
"description",
- "id",
- "jobs",
- "logo",
- "query",
- "tags",
- "title",
- "type"
+ "title"
],
- "uptime-dynamic-settings": [],
- "synthetics-privates-locations": [],
- "synthetics-monitor": [
- "alert",
- "alert.status",
- "alert.status.enabled",
- "alert.tls",
- "alert.tls.enabled",
- "custom_heartbeat_id",
+ "risk-engine-configuration": [
+ "dataViewId",
"enabled",
- "hash",
- "hosts",
- "id",
- "journey_id",
- "locations",
- "locations.id",
- "locations.label",
- "name",
- "origin",
- "project_id",
- "schedule",
- "schedule.number",
- "tags",
- "throttling",
- "throttling.label",
- "type",
- "urls"
- ],
- "uptime-synthetics-api-key": [
- "apiKey"
+ "filter",
+ "identifierType",
+ "interval",
+ "pageSize",
+ "range",
+ "range.end",
+ "range.start"
],
- "synthetics-param": [],
- "infrastructure-ui-source": [],
- "inventory-view": [],
- "metrics-explorer-view": [],
- "upgrade-assistant-reindex-operation": [
- "indexName",
- "status"
+ "rules-settings": [
+ "flapping"
],
- "upgrade-assistant-ml-upgrade-operation": [
- "snapshotId"
+ "sample-data-telemetry": [
+ "installCount",
+ "unInstallCount"
],
- "monitoring-telemetry": [
- "reportedClusterUuids"
+ "search": [
+ "description",
+ "title"
],
- "enterprise_search_telemetry": [],
- "app_search_telemetry": [],
- "workplace_search_telemetry": [],
- "siem-ui-timeline-note": [
+ "search-session": [
"created",
- "createdBy",
- "eventId",
- "note",
- "updated",
- "updatedBy"
+ "realmName",
+ "realmType",
+ "sessionId",
+ "username"
],
- "siem-ui-timeline-pinned-event": [
- "created",
- "createdBy",
- "eventId",
+ "search-telemetry": [],
+ "security-rule": [
+ "rule_id",
+ "version"
+ ],
+ "security-solution-signals-migration": [
+ "sourceIndex",
"updated",
- "updatedBy"
+ "version"
],
"siem-detection-engine-rule-actions": [
"actions",
@@ -830,10 +768,6 @@
"ruleAlertId",
"ruleThrottle"
],
- "security-rule": [
- "rule_id",
- "version"
- ],
"siem-ui-timeline": [
"columns",
"columns.aggregatable",
@@ -933,46 +867,112 @@
"updated",
"updatedBy"
],
- "endpoint:user-artifact-manifest": [
- "artifacts",
- "schemaVersion"
+ "siem-ui-timeline-note": [
+ "created",
+ "createdBy",
+ "eventId",
+ "note",
+ "updated",
+ "updatedBy"
],
- "security-solution-signals-migration": [
- "sourceIndex",
+ "siem-ui-timeline-pinned-event": [
+ "created",
+ "createdBy",
+ "eventId",
"updated",
- "version"
+ "updatedBy"
],
- "risk-engine-configuration": [
- "dataViewId",
+ "slo": [
+ "budgetingMethod",
+ "description",
"enabled",
- "filter",
- "identifierType",
- "interval",
- "pageSize",
- "range",
- "range.end",
- "range.start"
+ "id",
+ "indicator",
+ "indicator.params",
+ "indicator.type",
+ "name",
+ "tags",
+ "version"
],
- "policy-settings-protection-updates-note": [
- "note"
+ "space": [
+ "name"
],
- "apm-telemetry": [],
- "apm-server-schema": [
- "schemaJson"
+ "spaces-usage-stats": [],
+ "synthetics-monitor": [
+ "alert",
+ "alert.status",
+ "alert.status.enabled",
+ "alert.tls",
+ "alert.tls.enabled",
+ "custom_heartbeat_id",
+ "enabled",
+ "hash",
+ "hosts",
+ "id",
+ "journey_id",
+ "locations",
+ "locations.id",
+ "locations.label",
+ "name",
+ "origin",
+ "project_id",
+ "schedule",
+ "schedule.number",
+ "tags",
+ "throttling",
+ "throttling.label",
+ "type",
+ "urls"
],
- "apm-service-group": [
+ "synthetics-param": [],
+ "synthetics-privates-locations": [],
+ "tag": [
"color",
"description",
- "groupName",
- "kuery"
+ "name"
],
- "apm-custom-dashboards": [
- "dashboardSavedObjectId",
- "kuery",
- "serviceEnvironmentFilterEnabled",
- "serviceNameFilterEnabled"
+ "task": [
+ "attempts",
+ "enabled",
+ "ownerId",
+ "retryAt",
+ "runAt",
+ "schedule",
+ "schedule.interval",
+ "scheduledAt",
+ "scope",
+ "status",
+ "taskType"
],
- "cloud-security-posture-settings": [
- "rules"
- ]
+ "telemetry": [],
+ "threshold-explorer-view": [],
+ "ui-metric": [
+ "count"
+ ],
+ "upgrade-assistant-ml-upgrade-operation": [
+ "snapshotId"
+ ],
+ "upgrade-assistant-reindex-operation": [
+ "indexName",
+ "status"
+ ],
+ "uptime-dynamic-settings": [],
+ "uptime-synthetics-api-key": [
+ "apiKey"
+ ],
+ "url": [
+ "accessDate",
+ "createDate",
+ "slug"
+ ],
+ "usage-counters": [
+ "domainId"
+ ],
+ "visualization": [
+ "description",
+ "kibanaSavedObjectMeta",
+ "title",
+ "version"
+ ],
+ "workplace_search_telemetry": []
}
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index 4768e2605bb0b..758fde639d00f 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -1,1054 +1,907 @@
{
- "core-usage-stats": {
- "dynamic": false,
- "properties": {}
- },
- "legacy-url-alias": {
+ "action": {
"dynamic": false,
"properties": {
- "sourceId": {
- "type": "keyword"
- },
- "targetNamespace": {
- "type": "keyword"
- },
- "targetType": {
- "type": "keyword"
- },
- "targetId": {
+ "actionTypeId": {
"type": "keyword"
},
- "resolveCounter": {
- "type": "long"
- },
- "disabled": {
- "type": "boolean"
- }
- }
- },
- "config": {
- "dynamic": false,
- "properties": {
- "buildNum": {
- "type": "keyword"
- }
- }
- },
- "config-global": {
- "dynamic": false,
- "properties": {
- "buildNum": {
- "type": "keyword"
- }
- }
- },
- "url": {
- "dynamic": false,
- "properties": {
- "slug": {
- "type": "text",
+ "name": {
"fields": {
"keyword": {
"type": "keyword"
}
- }
- },
- "accessDate": {
- "type": "date"
- },
- "createDate": {
- "type": "date"
+ },
+ "type": "text"
}
}
},
- "usage-counters": {
+ "action_task_params": {
"dynamic": false,
- "properties": {
- "domainId": {
- "type": "keyword"
- }
- }
+ "properties": {}
},
- "task": {
+ "alert": {
"dynamic": false,
"properties": {
- "taskType": {
- "type": "keyword"
- },
- "scheduledAt": {
- "type": "date"
- },
- "runAt": {
- "type": "date"
- },
- "retryAt": {
- "type": "date"
- },
- "enabled": {
- "type": "boolean"
- },
- "schedule": {
+ "actions": {
+ "dynamic": false,
"properties": {
- "interval": {
+ "actionRef": {
"type": "keyword"
- }
- }
- },
- "attempts": {
- "type": "integer"
- },
- "status": {
- "type": "keyword"
- },
- "scope": {
- "type": "keyword"
- },
- "ownerId": {
- "type": "keyword"
- }
- }
- },
- "guided-onboarding-guide-state": {
- "dynamic": false,
- "properties": {
- "guideId": {
- "type": "keyword"
- },
- "isActive": {
- "type": "boolean"
- }
- }
- },
- "guided-onboarding-plugin-state": {
- "dynamic": false,
- "properties": {}
- },
- "ui-metric": {
- "properties": {
- "count": {
- "type": "integer"
- }
- }
- },
- "application_usage_totals": {
- "dynamic": false,
- "properties": {}
- },
- "application_usage_daily": {
- "dynamic": false,
- "properties": {
- "timestamp": {
- "type": "date"
- }
- }
- },
- "event_loop_delays_daily": {
- "dynamic": false,
- "properties": {
- "lastUpdatedAt": {
- "type": "date"
- }
- }
- },
- "index-pattern": {
- "dynamic": false,
- "properties": {
- "title": {
- "type": "text"
- },
- "type": {
- "type": "keyword"
- },
- "name": {
- "type": "text",
- "fields": {
- "keyword": {
+ },
+ "actionTypeId": {
+ "type": "keyword"
+ },
+ "group": {
"type": "keyword"
- }
- }
- }
- }
- },
- "sample-data-telemetry": {
- "properties": {
- "installCount": {
- "type": "long"
- },
- "unInstallCount": {
- "type": "long"
- }
- }
- },
- "space": {
- "dynamic": false,
- "properties": {
- "name": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 2048
- }
- }
- }
- }
- },
- "spaces-usage-stats": {
- "dynamic": false,
- "properties": {}
- },
- "exception-list-agnostic": {
- "properties": {
- "_tags": {
- "type": "keyword"
- },
- "created_at": {
- "type": "keyword"
- },
- "created_by": {
- "type": "keyword"
- },
- "description": {
- "type": "keyword"
- },
- "immutable": {
- "type": "boolean"
- },
- "list_id": {
- "type": "keyword"
- },
- "list_type": {
- "type": "keyword"
- },
- "meta": {
- "type": "keyword"
- },
- "name": {
- "fields": {
- "text": {
- "type": "text"
}
},
- "type": "keyword"
+ "type": "nested"
},
- "tags": {
- "fields": {
- "text": {
- "type": "text"
- }
- },
+ "alertTypeId": {
"type": "keyword"
},
- "tie_breaker_id": {
+ "consumer": {
"type": "keyword"
},
- "type": {
- "type": "keyword"
+ "createdAt": {
+ "type": "date"
},
- "updated_by": {
+ "createdBy": {
"type": "keyword"
},
- "version": {
- "type": "keyword"
+ "enabled": {
+ "type": "boolean"
},
- "comments": {
+ "executionStatus": {
"properties": {
- "comment": {
- "type": "keyword"
+ "error": {
+ "properties": {
+ "message": {
+ "type": "keyword"
+ },
+ "reason": {
+ "type": "keyword"
+ }
+ }
},
- "created_at": {
- "type": "keyword"
+ "lastDuration": {
+ "type": "long"
},
- "created_by": {
- "type": "keyword"
+ "lastExecutionDate": {
+ "type": "date"
},
- "id": {
- "type": "keyword"
+ "numberOfTriggeredActions": {
+ "type": "long"
},
- "updated_at": {
+ "status": {
"type": "keyword"
},
- "updated_by": {
- "type": "keyword"
- }
- }
- },
- "entries": {
- "properties": {
- "entries": {
+ "warning": {
"properties": {
- "field": {
- "type": "keyword"
- },
- "operator": {
- "type": "keyword"
- },
- "type": {
+ "message": {
"type": "keyword"
},
- "value": {
- "fields": {
- "text": {
- "type": "text"
- }
- },
+ "reason": {
"type": "keyword"
}
}
- },
- "field": {
- "type": "keyword"
- },
- "list": {
+ }
+ }
+ },
+ "lastRun": {
+ "properties": {
+ "alertsCount": {
"properties": {
- "id": {
- "type": "keyword"
+ "active": {
+ "type": "float"
},
- "type": {
- "type": "keyword"
+ "ignored": {
+ "type": "float"
+ },
+ "new": {
+ "type": "float"
+ },
+ "recovered": {
+ "type": "float"
}
}
},
- "operator": {
- "type": "keyword"
- },
- "type": {
+ "outcome": {
"type": "keyword"
},
- "value": {
- "fields": {
- "text": {
- "type": "text"
- }
- },
- "type": "keyword"
+ "outcomeOrder": {
+ "type": "float"
}
}
},
- "expire_time": {
- "type": "date"
- },
- "item_id": {
- "type": "keyword"
- },
- "os_types": {
- "type": "keyword"
- }
- }
- },
- "exception-list": {
- "properties": {
- "_tags": {
- "type": "keyword"
- },
- "created_at": {
+ "legacyId": {
"type": "keyword"
},
- "created_by": {
- "type": "keyword"
+ "mapped_params": {
+ "properties": {
+ "risk_score": {
+ "type": "float"
+ },
+ "severity": {
+ "type": "keyword"
+ }
+ }
},
- "description": {
- "type": "keyword"
+ "monitoring": {
+ "properties": {
+ "run": {
+ "properties": {
+ "calculated_metrics": {
+ "properties": {
+ "p50": {
+ "type": "long"
+ },
+ "p95": {
+ "type": "long"
+ },
+ "p99": {
+ "type": "long"
+ },
+ "success_ratio": {
+ "type": "float"
+ }
+ }
+ },
+ "last_run": {
+ "properties": {
+ "metrics": {
+ "properties": {
+ "duration": {
+ "type": "long"
+ },
+ "gap_duration_s": {
+ "type": "float"
+ },
+ "total_alerts_created": {
+ "type": "float"
+ },
+ "total_alerts_detected": {
+ "type": "float"
+ },
+ "total_indexing_duration_ms": {
+ "type": "long"
+ },
+ "total_search_duration_ms": {
+ "type": "long"
+ }
+ }
+ },
+ "timestamp": {
+ "type": "date"
+ }
+ }
+ }
+ }
+ }
+ }
},
- "immutable": {
+ "muteAll": {
"type": "boolean"
},
- "list_id": {
- "type": "keyword"
- },
- "list_type": {
- "type": "keyword"
- },
- "meta": {
+ "mutedInstanceIds": {
"type": "keyword"
},
"name": {
"fields": {
- "text": {
- "type": "text"
- }
- },
- "type": "keyword"
- },
- "tags": {
- "fields": {
- "text": {
- "type": "text"
+ "keyword": {
+ "normalizer": "lowercase",
+ "type": "keyword"
}
},
- "type": "keyword"
+ "type": "text"
},
- "tie_breaker_id": {
+ "notifyWhen": {
"type": "keyword"
},
- "type": {
- "type": "keyword"
+ "params": {
+ "ignore_above": 4096,
+ "type": "flattened"
},
- "updated_by": {
- "type": "keyword"
+ "revision": {
+ "type": "long"
},
- "version": {
- "type": "keyword"
+ "running": {
+ "type": "boolean"
},
- "comments": {
+ "schedule": {
"properties": {
- "comment": {
- "type": "keyword"
- },
- "created_at": {
- "type": "keyword"
- },
- "created_by": {
- "type": "keyword"
- },
- "id": {
- "type": "keyword"
- },
- "updated_at": {
- "type": "keyword"
- },
- "updated_by": {
+ "interval": {
"type": "keyword"
}
}
},
- "entries": {
+ "scheduledTaskId": {
+ "type": "keyword"
+ },
+ "snoozeSchedule": {
"properties": {
- "entries": {
- "properties": {
- "field": {
- "type": "keyword"
- },
- "operator": {
- "type": "keyword"
- },
- "type": {
- "type": "keyword"
- },
- "value": {
- "fields": {
- "text": {
- "type": "text"
- }
- },
- "type": "keyword"
- }
- }
- },
- "field": {
- "type": "keyword"
- },
- "list": {
- "properties": {
- "id": {
- "type": "keyword"
- },
- "type": {
- "type": "keyword"
- }
- }
- },
- "operator": {
- "type": "keyword"
+ "duration": {
+ "type": "long"
},
- "type": {
+ "id": {
"type": "keyword"
},
- "value": {
- "fields": {
- "text": {
- "type": "text"
- }
- },
- "type": "keyword"
+ "skipRecurrences": {
+ "format": "strict_date_time",
+ "type": "date"
}
- }
- },
- "expire_time": {
- "type": "date"
+ },
+ "type": "nested"
},
- "item_id": {
+ "tags": {
"type": "keyword"
},
- "os_types": {
+ "throttle": {
"type": "keyword"
- }
- }
- },
- "telemetry": {
- "dynamic": false,
- "properties": {}
- },
- "file": {
- "dynamic": false,
- "properties": {
- "created": {
- "type": "date"
},
- "Updated": {
+ "updatedAt": {
"type": "date"
},
- "name": {
- "type": "text"
- },
- "user": {
- "type": "flattened"
- },
- "Status": {
- "type": "keyword"
- },
- "mime_type": {
- "type": "keyword"
- },
- "extension": {
- "type": "keyword"
- },
- "size": {
- "type": "long"
- },
- "Meta": {
- "type": "flattened"
- },
- "FileKind": {
+ "updatedBy": {
"type": "keyword"
- },
- "hash": {
- "dynamic": false,
- "properties": {}
}
}
},
- "fileShare": {
- "dynamic": false,
+ "api_key_pending_invalidation": {
"properties": {
- "created": {
- "type": "date"
- },
- "valid_until": {
- "type": "long"
- },
- "token": {
+ "apiKeyId": {
"type": "keyword"
},
- "name": {
- "type": "keyword"
+ "createdAt": {
+ "type": "date"
}
}
},
- "action": {
- "dynamic": false,
+ "apm-custom-dashboards": {
"properties": {
- "name": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
- },
- "actionTypeId": {
+ "dashboardSavedObjectId": {
"type": "keyword"
+ },
+ "kuery": {
+ "type": "text"
+ },
+ "serviceEnvironmentFilterEnabled": {
+ "type": "boolean"
+ },
+ "serviceNameFilterEnabled": {
+ "type": "boolean"
}
}
},
- "action_task_params": {
+ "apm-indices": {
"dynamic": false,
"properties": {}
},
- "connector_token": {
- "dynamic": false,
+ "apm-server-schema": {
"properties": {
- "connectorId": {
- "type": "keyword"
- },
- "tokenType": {
- "type": "keyword"
+ "schemaJson": {
+ "index": false,
+ "type": "text"
}
}
},
- "query": {
- "dynamic": false,
+ "apm-service-group": {
"properties": {
- "title": {
+ "color": {
"type": "text"
},
"description": {
"type": "text"
+ },
+ "groupName": {
+ "type": "keyword"
+ },
+ "kuery": {
+ "type": "text"
}
}
},
- "kql-telemetry": {
+ "apm-telemetry": {
"dynamic": false,
"properties": {}
},
- "search-session": {
+ "app_search_telemetry": {
"dynamic": false,
- "properties": {
- "sessionId": {
- "type": "keyword"
- },
- "created": {
- "type": "date"
- },
- "realmType": {
- "type": "keyword"
- },
- "realmName": {
- "type": "keyword"
- },
- "username": {
- "type": "keyword"
+ "properties": {}
+ },
+ "application_usage_daily": {
+ "dynamic": false,
+ "properties": {
+ "timestamp": {
+ "type": "date"
}
}
},
- "search-telemetry": {
+ "application_usage_totals": {
"dynamic": false,
"properties": {}
},
- "file-upload-usage-collection-telemetry": {
+ "canvas-element": {
+ "dynamic": false,
"properties": {
- "file_upload": {
- "properties": {
- "index_creation_count": {
- "type": "long"
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "content": {
+ "type": "text"
+ },
+ "help": {
+ "type": "text"
+ },
+ "image": {
+ "type": "text"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
}
- }
+ },
+ "type": "text"
}
}
},
- "apm-indices": {
+ "canvas-workpad": {
"dynamic": false,
- "properties": {}
- },
- "tag": {
"properties": {
- "name": {
- "type": "text"
+ "@created": {
+ "type": "date"
},
- "description": {
- "type": "text"
+ "@timestamp": {
+ "type": "date"
},
- "color": {
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
"type": "text"
}
}
},
- "alert": {
+ "canvas-workpad-template": {
"dynamic": false,
"properties": {
- "enabled": {
- "type": "boolean"
+ "help": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
},
"name": {
- "type": "text",
"fields": {
"keyword": {
- "type": "keyword",
- "normalizer": "lowercase"
+ "type": "keyword"
}
- }
+ },
+ "type": "text"
},
"tags": {
- "type": "keyword"
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
},
- "alertTypeId": {
+ "template_key": {
"type": "keyword"
- },
- "schedule": {
+ }
+ }
+ },
+ "cases": {
+ "dynamic": false,
+ "properties": {
+ "assignees": {
"properties": {
- "interval": {
+ "uid": {
"type": "keyword"
}
}
},
- "consumer": {
+ "category": {
"type": "keyword"
},
- "legacyId": {
- "type": "keyword"
+ "closed_at": {
+ "type": "date"
},
- "actions": {
- "dynamic": false,
- "type": "nested",
+ "closed_by": {
"properties": {
- "group": {
+ "email": {
"type": "keyword"
},
- "actionRef": {
+ "full_name": {
"type": "keyword"
},
- "actionTypeId": {
+ "profile_uid": {
"type": "keyword"
- }
- }
- },
- "params": {
- "type": "flattened",
- "ignore_above": 4096
- },
- "mapped_params": {
- "properties": {
- "risk_score": {
- "type": "float"
},
- "severity": {
+ "username": {
"type": "keyword"
}
}
},
- "scheduledTaskId": {
- "type": "keyword"
- },
- "createdBy": {
- "type": "keyword"
- },
- "updatedBy": {
- "type": "keyword"
- },
- "createdAt": {
- "type": "date"
- },
- "updatedAt": {
- "type": "date"
- },
- "throttle": {
- "type": "keyword"
- },
- "notifyWhen": {
- "type": "keyword"
- },
- "muteAll": {
- "type": "boolean"
- },
- "mutedInstanceIds": {
- "type": "keyword"
- },
- "monitoring": {
+ "connector": {
"properties": {
- "run": {
+ "fields": {
"properties": {
- "calculated_metrics": {
- "properties": {
- "p50": {
- "type": "long"
- },
- "p95": {
- "type": "long"
- },
- "p99": {
- "type": "long"
- },
- "success_ratio": {
- "type": "float"
- }
- }
+ "key": {
+ "type": "text"
},
- "last_run": {
- "properties": {
- "timestamp": {
- "type": "date"
- },
- "metrics": {
- "properties": {
- "duration": {
- "type": "long"
- },
- "total_search_duration_ms": {
- "type": "long"
- },
- "total_indexing_duration_ms": {
- "type": "long"
- },
- "total_alerts_detected": {
- "type": "float"
- },
- "total_alerts_created": {
- "type": "float"
- },
- "gap_duration_s": {
- "type": "float"
- }
- }
- }
- }
+ "value": {
+ "type": "text"
}
}
+ },
+ "name": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
}
}
},
- "revision": {
- "type": "long"
+ "created_at": {
+ "type": "date"
},
- "snoozeSchedule": {
- "type": "nested",
+ "created_by": {
"properties": {
- "id": {
+ "email": {
"type": "keyword"
},
- "duration": {
- "type": "long"
+ "full_name": {
+ "type": "keyword"
},
- "skipRecurrences": {
- "type": "date",
- "format": "strict_date_time"
+ "profile_uid": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
}
}
},
- "executionStatus": {
+ "customFields": {
"properties": {
- "numberOfTriggeredActions": {
- "type": "long"
+ "key": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "fields": {
+ "boolean": {
+ "ignore_malformed": true,
+ "type": "boolean"
+ },
+ "date": {
+ "ignore_malformed": true,
+ "type": "date"
+ },
+ "ip": {
+ "ignore_malformed": true,
+ "type": "ip"
+ },
+ "number": {
+ "ignore_malformed": true,
+ "type": "long"
+ },
+ "string": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "description": {
+ "type": "text"
+ },
+ "duration": {
+ "type": "unsigned_long"
+ },
+ "external_service": {
+ "properties": {
+ "connector_name": {
+ "type": "keyword"
},
- "status": {
+ "external_id": {
"type": "keyword"
},
- "lastExecutionDate": {
- "type": "date"
+ "external_title": {
+ "type": "text"
},
- "lastDuration": {
- "type": "long"
+ "external_url": {
+ "type": "text"
},
- "error": {
+ "pushed_at": {
+ "type": "date"
+ },
+ "pushed_by": {
"properties": {
- "reason": {
+ "email": {
"type": "keyword"
},
- "message": {
+ "full_name": {
"type": "keyword"
- }
- }
- },
- "warning": {
- "properties": {
- "reason": {
+ },
+ "profile_uid": {
"type": "keyword"
},
- "message": {
+ "username": {
"type": "keyword"
}
}
}
}
},
- "lastRun": {
+ "owner": {
+ "type": "keyword"
+ },
+ "settings": {
"properties": {
- "outcome": {
+ "syncAlerts": {
+ "type": "boolean"
+ }
+ }
+ },
+ "severity": {
+ "type": "short"
+ },
+ "status": {
+ "type": "short"
+ },
+ "tags": {
+ "type": "keyword"
+ },
+ "title": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "total_alerts": {
+ "type": "integer"
+ },
+ "total_comments": {
+ "type": "integer"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "updated_by": {
+ "properties": {
+ "email": {
"type": "keyword"
},
- "outcomeOrder": {
- "type": "float"
+ "full_name": {
+ "type": "keyword"
},
- "alertsCount": {
- "properties": {
- "active": {
- "type": "float"
- },
- "new": {
- "type": "float"
- },
- "recovered": {
- "type": "float"
- },
- "ignored": {
- "type": "float"
- }
- }
+ "profile_uid": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
}
}
- },
- "running": {
- "type": "boolean"
}
}
},
- "api_key_pending_invalidation": {
+ "cases-comments": {
+ "dynamic": false,
"properties": {
- "apiKeyId": {
+ "actions": {
+ "properties": {
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "alertId": {
"type": "keyword"
},
- "createdAt": {
+ "comment": {
+ "type": "text"
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "created_by": {
+ "properties": {
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "externalReferenceAttachmentTypeId": {
+ "type": "keyword"
+ },
+ "owner": {
+ "type": "keyword"
+ },
+ "persistableStateAttachmentTypeId": {
+ "type": "keyword"
+ },
+ "pushed_at": {
+ "type": "date"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
"type": "date"
}
}
},
- "rules-settings": {
+ "cases-configure": {
"dynamic": false,
"properties": {
- "flapping": {
- "properties": {}
+ "closure_type": {
+ "type": "keyword"
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "owner": {
+ "type": "keyword"
}
}
},
- "maintenance-window": {
+ "cases-connector-mappings": {
"dynamic": false,
"properties": {
- "enabled": {
- "type": "boolean"
- },
- "events": {
- "type": "date_range",
- "format": "epoch_millis||strict_date_optional_time"
+ "owner": {
+ "type": "keyword"
}
}
},
- "graph-workspace": {
+ "cases-telemetry": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "cases-user-actions": {
+ "dynamic": false,
"properties": {
- "description": {
- "type": "text"
+ "action": {
+ "type": "keyword"
},
- "kibanaSavedObjectMeta": {
+ "created_at": {
+ "type": "date"
+ },
+ "created_by": {
"properties": {
- "searchSourceJSON": {
- "type": "text"
+ "username": {
+ "type": "keyword"
}
}
},
- "numLinks": {
- "type": "integer"
- },
- "numVertices": {
- "type": "integer"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
+ "owner": {
+ "type": "keyword"
},
- "wsState": {
- "type": "text"
+ "payload": {
+ "dynamic": false,
+ "properties": {
+ "assignees": {
+ "properties": {
+ "uid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "comment": {
+ "properties": {
+ "externalReferenceAttachmentTypeId": {
+ "type": "keyword"
+ },
+ "persistableStateAttachmentTypeId": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "connector": {
+ "properties": {
+ "type": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
},
- "legacyIndexPatternRef": {
- "type": "text",
- "index": false
+ "type": {
+ "type": "keyword"
}
}
},
- "search": {
+ "cloud-security-posture-settings": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "config": {
"dynamic": false,
"properties": {
- "title": {
- "type": "text"
- },
- "description": {
- "type": "text"
+ "buildNum": {
+ "type": "keyword"
}
}
},
- "visualization": {
+ "config-global": {
"dynamic": false,
"properties": {
- "description": {
- "type": "text"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {}
+ "buildNum": {
+ "type": "keyword"
}
}
},
- "event-annotation-group": {
+ "connector_token": {
"dynamic": false,
"properties": {
- "title": {
- "type": "text"
+ "connectorId": {
+ "type": "keyword"
},
- "description": {
- "type": "text"
+ "tokenType": {
+ "type": "keyword"
+ }
+ }
+ },
+ "core-usage-stats": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "csp-rule-template": {
+ "dynamic": false,
+ "properties": {
+ "metadata": {
+ "properties": {
+ "benchmark": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "posture_type": {
+ "type": "keyword"
+ },
+ "rule_number": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ },
+ "type": "object"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "section": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ },
+ "type": "object"
}
}
},
"dashboard": {
"properties": {
+ "controlGroupInput": {
+ "properties": {
+ "chainingSystem": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "controlStyle": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "ignoreParentSettingsJSON": {
+ "index": false,
+ "type": "text"
+ },
+ "panelsJSON": {
+ "index": false,
+ "type": "text"
+ }
+ }
+ },
"description": {
"type": "text"
},
"hits": {
- "type": "integer",
+ "doc_values": false,
"index": false,
- "doc_values": false
+ "type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
- "type": "text",
- "index": false
+ "index": false,
+ "type": "text"
}
}
},
"optionsJSON": {
- "type": "text",
- "index": false
+ "index": false,
+ "type": "text"
},
"panelsJSON": {
- "type": "text",
- "index": false
+ "index": false,
+ "type": "text"
},
"refreshInterval": {
"properties": {
"display": {
- "type": "keyword",
+ "doc_values": false,
"index": false,
- "doc_values": false
+ "type": "keyword"
},
"pause": {
- "type": "boolean",
+ "doc_values": false,
"index": false,
- "doc_values": false
+ "type": "boolean"
},
"section": {
- "type": "integer",
+ "doc_values": false,
"index": false,
- "doc_values": false
+ "type": "integer"
},
"value": {
- "type": "integer",
- "index": false,
- "doc_values": false
- }
- }
- },
- "controlGroupInput": {
- "properties": {
- "controlStyle": {
- "type": "keyword",
- "index": false,
- "doc_values": false
- },
- "chainingSystem": {
- "type": "keyword",
+ "doc_values": false,
"index": false,
- "doc_values": false
- },
- "panelsJSON": {
- "type": "text",
- "index": false
- },
- "ignoreParentSettingsJSON": {
- "type": "text",
- "index": false
+ "type": "integer"
}
}
},
"timeFrom": {
- "type": "keyword",
+ "doc_values": false,
"index": false,
- "doc_values": false
+ "type": "keyword"
},
"timeRestore": {
- "type": "boolean",
+ "doc_values": false,
"index": false,
- "doc_values": false
+ "type": "boolean"
},
"timeTo": {
- "type": "keyword",
+ "doc_values": false,
"index": false,
- "doc_values": false
+ "type": "keyword"
},
"title": {
"type": "text"
@@ -1058,536 +911,686 @@
}
}
},
- "links": {
+ "endpoint:user-artifact-manifest": {
"dynamic": false,
"properties": {
- "title": {
- "type": "text"
- },
- "description": {
- "type": "text"
+ "artifacts": {
+ "type": "nested"
},
- "links": {
- "dynamic": false,
- "properties": {}
+ "schemaVersion": {
+ "type": "keyword"
}
}
},
- "lens": {
+ "enterprise_search_telemetry": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "epm-packages": {
"properties": {
- "title": {
- "type": "text"
+ "es_index_patterns": {
+ "dynamic": false,
+ "properties": {}
},
- "description": {
- "type": "text"
+ "experimental_data_stream_features": {
+ "properties": {
+ "data_stream": {
+ "type": "keyword"
+ },
+ "features": {
+ "dynamic": false,
+ "properties": {
+ "synthetic_source": {
+ "type": "boolean"
+ },
+ "tsdb": {
+ "type": "boolean"
+ }
+ },
+ "type": "nested"
+ }
+ },
+ "type": "nested"
},
- "visualizationType": {
+ "install_format_schema_version": {
+ "type": "version"
+ },
+ "install_source": {
"type": "keyword"
},
- "state": {
- "dynamic": false,
- "properties": {}
- }
- }
- },
- "lens-ui-telemetry": {
- "properties": {
- "name": {
+ "install_started_at": {
+ "type": "date"
+ },
+ "install_status": {
"type": "keyword"
},
- "type": {
+ "install_version": {
"type": "keyword"
},
- "date": {
- "type": "date"
+ "installed_es": {
+ "properties": {
+ "deferred": {
+ "type": "boolean"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
},
- "count": {
- "type": "integer"
- }
- }
- },
- "map": {
- "properties": {
- "description": {
- "type": "text"
+ "installed_kibana": {
+ "dynamic": false,
+ "properties": {}
},
- "title": {
- "type": "text"
+ "installed_kibana_space_id": {
+ "type": "keyword"
},
- "version": {
- "type": "integer"
+ "internal": {
+ "type": "boolean"
},
- "mapStateJSON": {
- "type": "text"
+ "keep_policies_up_to_date": {
+ "index": false,
+ "type": "boolean"
},
- "layerListJSON": {
- "type": "text"
+ "latest_install_failed_attempts": {
+ "enabled": false,
+ "type": "object"
},
- "uiStateJSON": {
- "type": "text"
+ "name": {
+ "type": "keyword"
},
- "bounds": {
+ "package_assets": {
"dynamic": false,
"properties": {}
- }
- }
- },
- "cases-comments": {
- "dynamic": false,
- "properties": {
- "comment": {
- "type": "text"
},
- "owner": {
+ "verification_key_id": {
"type": "keyword"
},
- "type": {
+ "verification_status": {
"type": "keyword"
},
- "actions": {
- "properties": {
- "type": {
- "type": "keyword"
- }
- }
- },
- "alertId": {
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "epm-packages-assets": {
+ "properties": {
+ "asset_path": {
"type": "keyword"
},
- "created_at": {
- "type": "date"
+ "data_base64": {
+ "type": "binary"
},
- "created_by": {
- "properties": {
- "username": {
- "type": "keyword"
- }
- }
+ "data_utf8": {
+ "index": false,
+ "type": "text"
},
- "externalReferenceAttachmentTypeId": {
+ "install_source": {
"type": "keyword"
},
- "persistableStateAttachmentTypeId": {
+ "media_type": {
"type": "keyword"
},
- "pushed_at": {
- "type": "date"
+ "package_name": {
+ "type": "keyword"
},
- "updated_at": {
- "type": "date"
+ "package_version": {
+ "type": "keyword"
}
}
},
- "cases-configure": {
+ "event-annotation-group": {
"dynamic": false,
"properties": {
- "created_at": {
- "type": "date"
- },
- "closure_type": {
- "type": "keyword"
+ "description": {
+ "type": "text"
},
- "owner": {
- "type": "keyword"
+ "title": {
+ "type": "text"
}
}
},
- "cases-connector-mappings": {
+ "event_loop_delays_daily": {
"dynamic": false,
"properties": {
- "owner": {
- "type": "keyword"
+ "lastUpdatedAt": {
+ "type": "date"
}
}
},
- "cases": {
- "dynamic": false,
+ "exception-list": {
"properties": {
- "assignees": {
- "properties": {
- "uid": {
- "type": "keyword"
- }
- }
- },
- "closed_at": {
- "type": "date"
+ "_tags": {
+ "type": "keyword"
},
- "closed_by": {
+ "comments": {
"properties": {
- "username": {
+ "comment": {
"type": "keyword"
},
- "full_name": {
+ "created_at": {
"type": "keyword"
},
- "email": {
+ "created_by": {
"type": "keyword"
},
- "profile_uid": {
+ "id": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "keyword"
+ },
+ "updated_by": {
"type": "keyword"
}
}
},
"created_at": {
- "type": "date"
+ "type": "keyword"
},
"created_by": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "keyword"
+ },
+ "entries": {
"properties": {
- "username": {
+ "entries": {
+ "properties": {
+ "field": {
+ "type": "keyword"
+ },
+ "operator": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ }
+ }
+ },
+ "field": {
"type": "keyword"
},
- "full_name": {
+ "list": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "operator": {
"type": "keyword"
},
- "email": {
+ "type": {
"type": "keyword"
},
- "profile_uid": {
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
"type": "keyword"
}
}
},
- "duration": {
- "type": "unsigned_long"
+ "expire_time": {
+ "type": "date"
},
- "description": {
- "type": "text"
+ "immutable": {
+ "type": "boolean"
},
- "connector": {
- "properties": {
- "name": {
+ "item_id": {
+ "type": "keyword"
+ },
+ "list_id": {
+ "type": "keyword"
+ },
+ "list_type": {
+ "type": "keyword"
+ },
+ "meta": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "os_types": {
+ "type": "keyword"
+ },
+ "tags": {
+ "fields": {
+ "text": {
"type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "tie_breaker_id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_by": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "exception-list-agnostic": {
+ "properties": {
+ "_tags": {
+ "type": "keyword"
+ },
+ "comments": {
+ "properties": {
+ "comment": {
+ "type": "keyword"
},
- "type": {
+ "created_at": {
"type": "keyword"
},
- "fields": {
- "properties": {
- "key": {
- "type": "text"
- },
- "value": {
- "type": "text"
- }
- }
+ "created_by": {
+ "type": "keyword"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "keyword"
+ },
+ "updated_by": {
+ "type": "keyword"
}
}
},
- "external_service": {
+ "created_at": {
+ "type": "keyword"
+ },
+ "created_by": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "keyword"
+ },
+ "entries": {
"properties": {
- "pushed_at": {
- "type": "date"
- },
- "pushed_by": {
+ "entries": {
"properties": {
- "username": {
+ "field": {
"type": "keyword"
},
- "full_name": {
+ "operator": {
"type": "keyword"
},
- "email": {
+ "type": {
"type": "keyword"
},
- "profile_uid": {
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ }
+ }
+ },
+ "field": {
+ "type": "keyword"
+ },
+ "list": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
"type": "keyword"
}
}
},
- "connector_name": {
+ "operator": {
"type": "keyword"
},
- "external_id": {
+ "type": {
"type": "keyword"
},
- "external_title": {
- "type": "text"
- },
- "external_url": {
- "type": "text"
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
}
}
},
- "owner": {
+ "expire_time": {
+ "type": "date"
+ },
+ "immutable": {
+ "type": "boolean"
+ },
+ "item_id": {
"type": "keyword"
},
- "title": {
- "type": "text",
+ "list_id": {
+ "type": "keyword"
+ },
+ "list_type": {
+ "type": "keyword"
+ },
+ "meta": {
+ "type": "keyword"
+ },
+ "name": {
"fields": {
- "keyword": {
- "type": "keyword"
+ "text": {
+ "type": "text"
}
- }
+ },
+ "type": "keyword"
},
- "status": {
- "type": "short"
+ "os_types": {
+ "type": "keyword"
},
"tags": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
"type": "keyword"
},
- "updated_at": {
- "type": "date"
+ "tie_breaker_id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
},
"updated_by": {
- "properties": {
- "username": {
- "type": "keyword"
- },
- "full_name": {
- "type": "keyword"
- },
- "email": {
- "type": "keyword"
- },
- "profile_uid": {
- "type": "keyword"
- }
- }
+ "type": "keyword"
},
- "settings": {
- "properties": {
- "syncAlerts": {
- "type": "boolean"
- }
- }
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "file": {
+ "dynamic": false,
+ "properties": {
+ "FileKind": {
+ "type": "keyword"
},
- "severity": {
- "type": "short"
+ "Meta": {
+ "type": "flattened"
},
- "total_alerts": {
- "type": "integer"
+ "Status": {
+ "type": "keyword"
},
- "total_comments": {
- "type": "integer"
+ "Updated": {
+ "type": "date"
},
- "category": {
+ "created": {
+ "type": "date"
+ },
+ "extension": {
"type": "keyword"
},
- "customFields": {
- "type": "nested",
+ "hash": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "mime_type": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "text"
+ },
+ "size": {
+ "type": "long"
+ },
+ "user": {
+ "type": "flattened"
+ }
+ }
+ },
+ "file-upload-usage-collection-telemetry": {
+ "properties": {
+ "file_upload": {
"properties": {
- "key": {
- "type": "keyword"
- },
- "type": {
- "type": "keyword"
- },
- "value": {
- "type": "keyword",
- "fields": {
- "number": {
- "type": "long",
- "ignore_malformed": true
- },
- "boolean": {
- "type": "boolean",
- "ignore_malformed": true
- },
- "string": {
- "type": "text"
- },
- "date": {
- "type": "date",
- "ignore_malformed": true
- },
- "ip": {
- "type": "ip",
- "ignore_malformed": true
- }
- }
+ "index_creation_count": {
+ "type": "long"
}
}
}
}
},
- "cases-user-actions": {
+ "fileShare": {
"dynamic": false,
"properties": {
- "action": {
+ "created": {
+ "type": "date"
+ },
+ "name": {
"type": "keyword"
},
- "created_at": {
- "type": "date"
+ "token": {
+ "type": "keyword"
},
- "created_by": {
- "properties": {
- "username": {
- "type": "keyword"
- }
- }
+ "valid_until": {
+ "type": "long"
+ }
+ }
+ },
+ "fleet-fleet-server-host": {
+ "properties": {
+ "host_urls": {
+ "index": false,
+ "type": "keyword"
},
- "payload": {
- "dynamic": false,
- "properties": {
- "connector": {
- "properties": {
- "type": {
- "type": "keyword"
- }
- }
- },
- "comment": {
- "properties": {
- "type": {
- "type": "keyword"
- },
- "externalReferenceAttachmentTypeId": {
- "type": "keyword"
- },
- "persistableStateAttachmentTypeId": {
- "type": "keyword"
- }
- }
- },
- "assignees": {
- "properties": {
- "uid": {
- "type": "keyword"
- }
- }
- }
- }
+ "is_default": {
+ "type": "boolean"
},
- "owner": {
+ "is_internal": {
+ "index": false,
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "proxy_id": {
+ "type": "keyword"
+ }
+ }
+ },
+ "fleet-message-signing-keys": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "fleet-preconfiguration-deletion-record": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ }
+ }
+ },
+ "fleet-proxy": {
+ "properties": {
+ "certificate": {
+ "index": false,
+ "type": "keyword"
+ },
+ "certificate_authorities": {
+ "index": false,
+ "type": "keyword"
+ },
+ "certificate_key": {
+ "index": false,
+ "type": "keyword"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ },
+ "name": {
"type": "keyword"
},
- "type": {
+ "proxy_headers": {
+ "index": false,
+ "type": "text"
+ },
+ "url": {
+ "index": false,
"type": "keyword"
}
}
},
- "cases-telemetry": {
- "dynamic": false,
- "properties": {}
- },
- "infrastructure-monitoring-log-view": {
+ "fleet-uninstall-tokens": {
"dynamic": false,
"properties": {
- "name": {
- "type": "text"
+ "policy_id": {
+ "type": "keyword"
+ },
+ "token_plain": {
+ "type": "keyword"
}
}
},
- "metrics-data-source": {
- "dynamic": false,
- "properties": {}
- },
- "canvas-element": {
- "dynamic": false,
+ "graph-workspace": {
"properties": {
- "name": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
}
}
},
- "help": {
+ "legacyIndexPatternRef": {
+ "index": false,
"type": "text"
},
- "content": {
- "type": "text"
+ "numLinks": {
+ "type": "integer"
},
- "image": {
+ "numVertices": {
+ "type": "integer"
+ },
+ "title": {
"type": "text"
},
- "@timestamp": {
- "type": "date"
+ "version": {
+ "type": "integer"
},
- "@created": {
- "type": "date"
+ "wsState": {
+ "type": "text"
}
}
},
- "canvas-workpad": {
+ "guided-onboarding-guide-state": {
"dynamic": false,
"properties": {
- "name": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
- },
- "@timestamp": {
- "type": "date"
+ "guideId": {
+ "type": "keyword"
},
- "@created": {
- "type": "date"
+ "isActive": {
+ "type": "boolean"
}
}
},
- "canvas-workpad-template": {
+ "guided-onboarding-plugin-state": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "index-pattern": {
"dynamic": false,
"properties": {
"name": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
- },
- "help": {
- "type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
- }
+ },
+ "type": "text"
},
- "tags": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
+ "title": {
+ "type": "text"
},
- "template_key": {
+ "type": {
"type": "keyword"
}
}
},
- "ingest_manager_settings": {
+ "infrastructure-monitoring-log-view": {
+ "dynamic": false,
"properties": {
- "fleet_server_hosts": {
- "type": "keyword"
- },
- "has_seen_add_data_notice": {
- "type": "boolean",
- "index": false
- },
- "prerelease_integrations_enabled": {
- "type": "boolean"
- },
- "output_secret_storage_requirements_met": {
- "type": "boolean"
- },
- "secret_storage_requirements_met": {
- "type": "boolean"
+ "name": {
+ "type": "text"
}
}
},
+ "infrastructure-ui-source": {
+ "dynamic": false,
+ "properties": {}
+ },
"ingest-agent-policies": {
"properties": {
- "name": {
- "type": "keyword"
+ "agent_features": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "keyword"
+ }
+ }
},
- "schema_version": {
- "type": "version"
+ "data_output_id": {
+ "type": "keyword"
},
"description": {
"type": "text"
},
- "namespace": {
+ "download_source_id": {
"type": "keyword"
},
- "is_managed": {
- "type": "boolean"
+ "fleet_server_host_id": {
+ "type": "keyword"
+ },
+ "inactivity_timeout": {
+ "type": "integer"
},
"is_default": {
"type": "boolean"
@@ -1595,97 +1598,111 @@
"is_default_fleet_server": {
"type": "boolean"
},
- "status": {
+ "is_managed": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
"type": "keyword"
},
- "unenroll_timeout": {
- "type": "integer"
+ "is_protected": {
+ "type": "boolean"
},
- "inactivity_timeout": {
- "type": "integer"
+ "keep_monitoring_alive": {
+ "type": "boolean"
},
- "updated_at": {
- "type": "date"
+ "monitoring_enabled": {
+ "index": false,
+ "type": "keyword"
},
- "updated_by": {
+ "monitoring_output_id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "namespace": {
"type": "keyword"
},
+ "overrides": {
+ "index": false,
+ "type": "flattened"
+ },
"revision": {
"type": "integer"
},
- "monitoring_enabled": {
- "type": "keyword",
- "index": false
+ "schema_version": {
+ "type": "version"
},
- "is_preconfigured": {
+ "status": {
"type": "keyword"
},
- "data_output_id": {
- "type": "keyword"
+ "unenroll_timeout": {
+ "type": "integer"
},
- "monitoring_output_id": {
- "type": "keyword"
+ "updated_at": {
+ "type": "date"
},
- "download_source_id": {
+ "updated_by": {
"type": "keyword"
- },
- "fleet_server_host_id": {
+ }
+ }
+ },
+ "ingest-download-sources": {
+ "properties": {
+ "host": {
"type": "keyword"
},
- "agent_features": {
- "properties": {
- "name": {
- "type": "keyword"
- },
- "enabled": {
- "type": "boolean"
- }
- }
- },
- "is_protected": {
+ "is_default": {
"type": "boolean"
},
- "overrides": {
- "type": "flattened",
- "index": false
+ "name": {
+ "type": "keyword"
},
- "keep_monitoring_alive": {
- "type": "boolean"
+ "proxy_id": {
+ "type": "keyword"
+ },
+ "source_id": {
+ "index": false,
+ "type": "keyword"
}
}
},
"ingest-outputs": {
"properties": {
- "output_id": {
- "type": "keyword",
- "index": false
- },
- "name": {
- "type": "keyword"
+ "allow_edit": {
+ "enabled": false
},
- "type": {
+ "auth_type": {
"type": "keyword"
},
- "is_default": {
- "type": "boolean"
+ "broker_ack_reliability": {
+ "type": "text"
},
- "is_default_monitoring": {
- "type": "boolean"
+ "broker_buffer_size": {
+ "type": "integer"
},
- "hosts": {
- "type": "keyword"
+ "broker_timeout": {
+ "type": "integer"
},
"ca_sha256": {
- "type": "keyword",
- "index": false
+ "index": false,
+ "type": "keyword"
},
"ca_trusted_fingerprint": {
- "type": "keyword",
- "index": false
+ "index": false,
+ "type": "keyword"
+ },
+ "channel_buffer_size": {
+ "type": "integer"
+ },
+ "client_id": {
+ "type": "keyword"
+ },
+ "compression": {
+ "type": "keyword"
},
- "service_token": {
- "type": "keyword",
- "index": false
+ "compression_level": {
+ "type": "integer"
},
"config": {
"type": "flattened"
@@ -1693,64 +1710,70 @@
"config_yaml": {
"type": "text"
},
- "is_preconfigured": {
- "type": "boolean",
- "index": false
- },
- "is_internal": {
- "type": "boolean",
- "index": false
- },
- "ssl": {
- "type": "binary"
- },
- "proxy_id": {
+ "connection_type": {
"type": "keyword"
},
- "shipper": {
+ "hash": {
"dynamic": false,
- "properties": {}
+ "properties": {
+ "hash": {
+ "type": "text"
+ },
+ "random": {
+ "type": "boolean"
+ }
+ }
},
- "allow_edit": {
- "enabled": false
+ "headers": {
+ "dynamic": false,
+ "properties": {
+ "key": {
+ "type": "text"
+ },
+ "value": {
+ "type": "text"
+ }
+ }
},
- "version": {
+ "hosts": {
"type": "keyword"
},
- "key": {
- "type": "keyword"
+ "is_default": {
+ "type": "boolean"
},
- "compression": {
- "type": "keyword"
+ "is_default_monitoring": {
+ "type": "boolean"
},
- "compression_level": {
- "type": "integer"
+ "is_internal": {
+ "index": false,
+ "type": "boolean"
},
- "client_id": {
+ "is_preconfigured": {
+ "index": false,
+ "type": "boolean"
+ },
+ "key": {
"type": "keyword"
},
- "auth_type": {
+ "name": {
"type": "keyword"
},
- "connection_type": {
+ "output_id": {
+ "index": false,
"type": "keyword"
},
- "username": {
+ "partition": {
"type": "keyword"
},
"password": {
- "type": "text",
- "index": false
+ "index": false,
+ "type": "text"
},
- "sasl": {
- "dynamic": false,
- "properties": {
- "mechanism": {
- "type": "text"
- }
- }
+ "preset": {
+ "index": false,
+ "type": "keyword"
},
- "partition": {
+ "proxy_id": {
"type": "keyword"
},
"random": {
@@ -1761,6 +1784,9 @@
}
}
},
+ "required_acks": {
+ "type": "integer"
+ },
"round_robin": {
"dynamic": false,
"properties": {
@@ -1769,69 +1795,26 @@
}
}
},
- "hash": {
+ "sasl": {
"dynamic": false,
"properties": {
- "hash": {
+ "mechanism": {
"type": "text"
- },
- "random": {
- "type": "boolean"
}
}
},
- "topics": {
+ "secrets": {
"dynamic": false,
"properties": {
- "topic": {
- "type": "keyword"
- },
- "when": {
+ "password": {
"dynamic": false,
"properties": {
- "type": {
- "type": "text"
- },
- "condition": {
- "type": "text"
+ "id": {
+ "type": "keyword"
}
}
- }
- }
- },
- "headers": {
- "dynamic": false,
- "properties": {
- "key": {
- "type": "text"
},
- "value": {
- "type": "text"
- }
- }
- },
- "timeout": {
- "type": "integer"
- },
- "broker_timeout": {
- "type": "integer"
- },
- "broker_ack_reliability": {
- "type": "text"
- },
- "broker_buffer_size": {
- "type": "integer"
- },
- "required_acks": {
- "type": "integer"
- },
- "channel_buffer_size": {
- "type": "integer"
- },
- "secrets": {
- "dynamic": false,
- "properties": {
- "password": {
+ "service_token": {
"dynamic": false,
"properties": {
"id": {
@@ -1851,41 +1834,82 @@
}
}
}
+ }
+ }
+ },
+ "service_token": {
+ "index": false,
+ "type": "keyword"
+ },
+ "shipper": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "ssl": {
+ "type": "binary"
+ },
+ "timeout": {
+ "type": "integer"
+ },
+ "topics": {
+ "dynamic": false,
+ "properties": {
+ "topic": {
+ "type": "keyword"
},
- "service_token": {
+ "when": {
"dynamic": false,
"properties": {
- "id": {
- "type": "keyword"
+ "condition": {
+ "type": "text"
+ },
+ "type": {
+ "type": "text"
}
}
}
}
},
- "preset": {
- "type": "keyword",
- "index": false
+ "type": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
}
}
},
"ingest-package-policies": {
"properties": {
- "name": {
+ "created_at": {
+ "type": "date"
+ },
+ "created_by": {
"type": "keyword"
},
"description": {
"type": "text"
},
- "namespace": {
- "type": "keyword"
+ "elasticsearch": {
+ "dynamic": false,
+ "properties": {}
},
"enabled": {
"type": "boolean"
},
+ "inputs": {
+ "dynamic": false,
+ "properties": {}
+ },
"is_managed": {
"type": "boolean"
},
- "policy_id": {
+ "name": {
+ "type": "keyword"
+ },
+ "namespace": {
"type": "keyword"
},
"package": {
@@ -1901,16 +1925,11 @@
}
}
},
- "elasticsearch": {
- "dynamic": false,
- "properties": {}
- },
- "vars": {
- "type": "flattened"
+ "policy_id": {
+ "type": "keyword"
},
- "inputs": {
- "dynamic": false,
- "properties": {}
+ "revision": {
+ "type": "integer"
},
"secret_references": {
"properties": {
@@ -1919,235 +1938,295 @@
}
}
},
- "revision": {
- "type": "integer"
+ "updated_at": {
+ "type": "date"
+ },
+ "updated_by": {
+ "type": "keyword"
+ },
+ "vars": {
+ "type": "flattened"
+ }
+ }
+ },
+ "ingest_manager_settings": {
+ "properties": {
+ "fleet_server_hosts": {
+ "type": "keyword"
+ },
+ "has_seen_add_data_notice": {
+ "index": false,
+ "type": "boolean"
+ },
+ "output_secret_storage_requirements_met": {
+ "type": "boolean"
+ },
+ "prerelease_integrations_enabled": {
+ "type": "boolean"
+ },
+ "secret_storage_requirements_met": {
+ "type": "boolean"
+ }
+ }
+ },
+ "inventory-view": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "kql-telemetry": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "legacy-url-alias": {
+ "dynamic": false,
+ "properties": {
+ "disabled": {
+ "type": "boolean"
+ },
+ "resolveCounter": {
+ "type": "long"
},
- "updated_at": {
- "type": "date"
+ "sourceId": {
+ "type": "keyword"
},
- "updated_by": {
+ "targetId": {
"type": "keyword"
},
- "created_at": {
- "type": "date"
+ "targetNamespace": {
+ "type": "keyword"
},
- "created_by": {
+ "targetType": {
"type": "keyword"
}
}
},
- "epm-packages": {
+ "lens": {
"properties": {
- "name": {
- "type": "keyword"
- },
- "version": {
- "type": "keyword"
- },
- "internal": {
- "type": "boolean"
- },
- "keep_policies_up_to_date": {
- "type": "boolean",
- "index": false
+ "description": {
+ "type": "text"
},
- "es_index_patterns": {
+ "state": {
"dynamic": false,
"properties": {}
},
- "verification_status": {
- "type": "keyword"
- },
- "verification_key_id": {
- "type": "keyword"
- },
- "installed_es": {
- "type": "nested",
- "properties": {
- "id": {
- "type": "keyword"
- },
- "type": {
- "type": "keyword"
- },
- "version": {
- "type": "keyword"
- },
- "deferred": {
- "type": "boolean"
- }
- }
- },
- "latest_install_failed_attempts": {
- "type": "object",
- "enabled": false
- },
- "installed_kibana": {
- "dynamic": false,
- "properties": {}
+ "title": {
+ "type": "text"
},
- "installed_kibana_space_id": {
+ "visualizationType": {
"type": "keyword"
+ }
+ }
+ },
+ "lens-ui-telemetry": {
+ "properties": {
+ "count": {
+ "type": "integer"
},
- "package_assets": {
- "dynamic": false,
- "properties": {}
- },
- "install_started_at": {
+ "date": {
"type": "date"
},
- "install_version": {
+ "name": {
"type": "keyword"
},
- "install_status": {
+ "type": {
"type": "keyword"
+ }
+ }
+ },
+ "links": {
+ "dynamic": false,
+ "properties": {
+ "description": {
+ "type": "text"
},
- "install_source": {
- "type": "keyword"
+ "links": {
+ "dynamic": false,
+ "properties": {}
},
- "install_format_schema_version": {
- "type": "version"
+ "title": {
+ "type": "text"
+ }
+ }
+ },
+ "maintenance-window": {
+ "dynamic": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
},
- "experimental_data_stream_features": {
- "type": "nested",
- "properties": {
- "data_stream": {
- "type": "keyword"
- },
- "features": {
- "type": "nested",
- "dynamic": false,
- "properties": {
- "synthetic_source": {
- "type": "boolean"
- },
- "tsdb": {
- "type": "boolean"
- }
- }
- }
- }
+ "events": {
+ "format": "epoch_millis||strict_date_optional_time",
+ "type": "date_range"
}
}
},
- "epm-packages-assets": {
+ "map": {
"properties": {
- "package_name": {
- "type": "keyword"
+ "bounds": {
+ "dynamic": false,
+ "properties": {}
},
- "package_version": {
- "type": "keyword"
+ "description": {
+ "type": "text"
},
- "install_source": {
- "type": "keyword"
+ "layerListJSON": {
+ "type": "text"
},
- "asset_path": {
- "type": "keyword"
+ "mapStateJSON": {
+ "type": "text"
},
- "media_type": {
- "type": "keyword"
+ "title": {
+ "type": "text"
},
- "data_utf8": {
- "type": "text",
- "index": false
+ "uiStateJSON": {
+ "type": "text"
},
- "data_base64": {
- "type": "binary"
+ "version": {
+ "type": "integer"
}
}
},
- "fleet-preconfiguration-deletion-record": {
+ "metrics-data-source": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "metrics-explorer-view": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "ml-job": {
"properties": {
- "id": {
+ "datafeed_id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "job_id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "type": {
"type": "keyword"
}
}
},
- "ingest-download-sources": {
+ "ml-module": {
+ "dynamic": false,
"properties": {
- "source_id": {
- "type": "keyword",
- "index": false
+ "datafeeds": {
+ "type": "object"
},
- "name": {
- "type": "keyword"
+ "defaultIndexPattern": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
},
- "is_default": {
- "type": "boolean"
+ "description": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
},
- "host": {
- "type": "keyword"
+ "id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
},
- "proxy_id": {
- "type": "keyword"
- }
- }
- },
- "fleet-fleet-server-host": {
- "properties": {
- "name": {
- "type": "keyword"
+ "jobs": {
+ "type": "object"
},
- "is_default": {
- "type": "boolean"
+ "logo": {
+ "type": "object"
},
- "is_internal": {
- "type": "boolean",
- "index": false
+ "query": {
+ "type": "object"
},
- "host_urls": {
- "type": "keyword",
- "index": false
+ "tags": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
},
- "is_preconfigured": {
- "type": "boolean"
+ "title": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
},
- "proxy_id": {
- "type": "keyword"
+ "type": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
}
}
},
- "fleet-proxy": {
+ "ml-trained-model": {
"properties": {
- "name": {
- "type": "keyword"
- },
- "url": {
- "type": "keyword",
- "index": false
- },
- "proxy_headers": {
- "type": "text",
- "index": false
- },
- "certificate_authorities": {
- "type": "keyword",
- "index": false
- },
- "certificate": {
- "type": "keyword",
- "index": false
- },
- "certificate_key": {
- "type": "keyword",
- "index": false
+ "job": {
+ "properties": {
+ "create_time": {
+ "type": "date"
+ },
+ "job_id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
},
- "is_preconfigured": {
- "type": "boolean"
+ "model_id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
}
}
},
- "fleet-message-signing-keys": {
- "dynamic": false,
- "properties": {}
- },
- "fleet-uninstall-tokens": {
- "dynamic": false,
+ "monitoring-telemetry": {
"properties": {
- "policy_id": {
+ "reportedClusterUuids": {
"type": "keyword"
+ }
+ }
+ },
+ "observability-onboarding-state": {
+ "properties": {
+ "progress": {
+ "dynamic": false,
+ "type": "object"
},
- "token_plain": {
+ "state": {
+ "dynamic": false,
+ "type": "object"
+ },
+ "type": {
"type": "keyword"
}
}
@@ -2162,237 +2241,58 @@
}
}
},
- "osquery-saved-query": {
- "dynamic": false,
+ "osquery-pack": {
"properties": {
- "description": {
- "type": "text"
- },
- "id": {
- "type": "keyword"
- },
- "query": {
- "type": "text"
- },
"created_at": {
"type": "date"
},
"created_by": {
- "type": "text"
- },
- "platform": {
- "type": "keyword"
- },
- "version": {
- "type": "keyword"
- },
- "updated_at": {
- "type": "date"
- },
- "updated_by": {
- "type": "text"
- },
- "interval": {
"type": "keyword"
},
- "timeout": {
- "type": "short"
- },
- "ecs_mapping": {
- "dynamic": false,
- "properties": {}
- }
- }
- },
- "osquery-pack": {
- "properties": {
"description": {
"type": "text"
},
- "name": {
- "type": "text"
- },
- "created_at": {
- "type": "date"
- },
- "created_by": {
- "type": "keyword"
- },
- "updated_at": {
- "type": "date"
- },
- "updated_by": {
- "type": "keyword"
- },
"enabled": {
"type": "boolean"
},
- "shards": {
- "dynamic": false,
- "properties": {}
- },
- "version": {
- "type": "long"
+ "name": {
+ "type": "text"
},
"queries": {
"dynamic": false,
"properties": {
+ "ecs_mapping": {
+ "dynamic": false,
+ "properties": {}
+ },
"id": {
"type": "keyword"
},
- "query": {
- "type": "text"
- },
"interval": {
"type": "text"
},
- "timeout": {
- "type": "short"
- },
"platform": {
"type": "keyword"
},
- "version": {
- "type": "keyword"
- },
- "ecs_mapping": {
- "dynamic": false,
- "properties": {}
- }
- }
- }
- }
- },
- "osquery-pack-asset": {
- "dynamic": false,
- "properties": {
- "description": {
- "type": "text"
- },
- "name": {
- "type": "text"
- },
- "version": {
- "type": "long"
- },
- "shards": {
- "dynamic": false,
- "properties": {}
- },
- "queries": {
- "dynamic": false,
- "properties": {
- "id": {
- "type": "keyword"
- },
"query": {
"type": "text"
},
- "interval": {
- "type": "text"
- },
"timeout": {
"type": "short"
},
- "platform": {
- "type": "keyword"
- },
- "version": {
- "type": "keyword"
- },
- "ecs_mapping": {
- "dynamic": false,
- "properties": {}
- }
- }
- }
- }
- },
- "csp-rule-template": {
- "dynamic": false,
- "properties": {
- "metadata": {
- "type": "object",
- "properties": {
- "name": {
- "type": "keyword",
- "fields": {
- "text": {
- "type": "text"
- }
- }
- },
- "id": {
- "type": "keyword"
- },
- "section": {
- "type": "keyword",
- "fields": {
- "text": {
- "type": "text"
- }
- }
- },
"version": {
"type": "keyword"
- },
- "benchmark": {
- "type": "object",
- "properties": {
- "id": {
- "type": "keyword"
- },
- "name": {
- "type": "keyword"
- },
- "posture_type": {
- "type": "keyword"
- },
- "version": {
- "type": "keyword"
- },
- "rule_number": {
- "type": "keyword"
- }
- }
- }
- }
- }
- }
- },
- "cloud-security-posture-settings": {
- "dynamic": false,
- "properties": {}
- },
- "slo": {
- "dynamic": false,
- "properties": {
- "id": {
- "type": "keyword"
- },
- "name": {
- "type": "text"
- },
- "description": {
- "type": "text"
- },
- "indicator": {
- "properties": {
- "type": {
- "type": "keyword"
- },
- "params": {
- "type": "flattened"
}
}
},
- "budgetingMethod": {
- "type": "keyword"
+ "shards": {
+ "dynamic": false,
+ "properties": {}
},
- "enabled": {
- "type": "boolean"
+ "updated_at": {
+ "type": "date"
},
- "tags": {
+ "updated_by": {
"type": "keyword"
},
"version": {
@@ -2400,404 +2300,238 @@
}
}
},
- "threshold-explorer-view": {
+ "osquery-pack-asset": {
"dynamic": false,
- "properties": {}
- },
- "observability-onboarding-state": {
"properties": {
- "type": {
- "type": "keyword"
+ "description": {
+ "type": "text"
},
- "state": {
- "type": "object",
- "dynamic": false
+ "name": {
+ "type": "text"
},
- "progress": {
- "type": "object",
- "dynamic": false
- }
- }
- },
- "ml-job": {
- "properties": {
- "job_id": {
- "type": "text",
- "fields": {
- "keyword": {
+ "queries": {
+ "dynamic": false,
+ "properties": {
+ "ecs_mapping": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "id": {
"type": "keyword"
- }
- }
- },
- "datafeed_id": {
- "type": "text",
- "fields": {
- "keyword": {
+ },
+ "interval": {
+ "type": "text"
+ },
+ "platform": {
"type": "keyword"
- }
- }
- },
- "type": {
- "type": "keyword"
- }
- }
- },
- "ml-trained-model": {
- "properties": {
- "model_id": {
- "type": "text",
- "fields": {
- "keyword": {
+ },
+ "query": {
+ "type": "text"
+ },
+ "timeout": {
+ "type": "short"
+ },
+ "version": {
"type": "keyword"
}
}
},
- "job": {
- "properties": {
- "job_id": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
- },
- "create_time": {
- "type": "date"
- }
- }
+ "shards": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "version": {
+ "type": "long"
}
}
},
- "ml-module": {
+ "osquery-saved-query": {
"dynamic": false,
"properties": {
- "id": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
+ "created_at": {
+ "type": "date"
},
- "title": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
+ "created_by": {
+ "type": "text"
},
"description": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
+ "type": "text"
},
- "type": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
+ "ecs_mapping": {
+ "dynamic": false,
+ "properties": {}
},
- "logo": {
- "type": "object"
+ "id": {
+ "type": "keyword"
},
- "defaultIndexPattern": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
+ "interval": {
+ "type": "keyword"
+ },
+ "platform": {
+ "type": "keyword"
},
"query": {
- "type": "object"
+ "type": "text"
},
- "jobs": {
- "type": "object"
+ "timeout": {
+ "type": "short"
},
- "datafeeds": {
- "type": "object"
+ "updated_at": {
+ "type": "date"
},
- "tags": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword"
- }
- }
+ "updated_by": {
+ "type": "text"
+ },
+ "version": {
+ "type": "keyword"
}
}
},
- "uptime-dynamic-settings": {
- "dynamic": false,
- "properties": {}
+ "policy-settings-protection-updates-note": {
+ "properties": {
+ "note": {
+ "index": false,
+ "type": "text"
+ }
+ }
},
- "synthetics-privates-locations": {
+ "query": {
"dynamic": false,
- "properties": {}
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ }
+ }
},
- "synthetics-monitor": {
+ "risk-engine-configuration": {
"dynamic": false,
"properties": {
- "name": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256,
- "normalizer": "lowercase"
- }
- }
- },
- "type": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- },
- "urls": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- },
- "hosts": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- },
- "journey_id": {
- "type": "keyword"
- },
- "project_id": {
- "type": "keyword",
- "fields": {
- "text": {
- "type": "text"
- }
- }
- },
- "origin": {
- "type": "keyword"
- },
- "hash": {
- "type": "keyword"
- },
- "locations": {
- "properties": {
- "id": {
- "type": "keyword",
- "ignore_above": 256,
- "fields": {
- "text": {
- "type": "text"
- }
- }
- },
- "label": {
- "type": "text"
- }
- }
- },
- "custom_heartbeat_id": {
+ "dataViewId": {
"type": "keyword"
},
- "id": {
- "type": "keyword"
+ "enabled": {
+ "type": "boolean"
},
- "tags": {
- "type": "keyword",
- "fields": {
- "text": {
- "type": "text"
- }
- }
+ "filter": {
+ "dynamic": false,
+ "properties": {}
},
- "schedule": {
- "properties": {
- "number": {
- "type": "integer"
- }
- }
+ "identifierType": {
+ "type": "keyword"
},
- "enabled": {
- "type": "boolean"
+ "interval": {
+ "type": "keyword"
},
- "alert": {
- "properties": {
- "status": {
- "properties": {
- "enabled": {
- "type": "boolean"
- }
- }
- },
- "tls": {
- "properties": {
- "enabled": {
- "type": "boolean"
- }
- }
- }
- }
+ "pageSize": {
+ "type": "integer"
},
- "throttling": {
+ "range": {
"properties": {
- "label": {
+ "end": {
+ "type": "keyword"
+ },
+ "start": {
"type": "keyword"
}
}
}
}
},
- "uptime-synthetics-api-key": {
+ "rules-settings": {
"dynamic": false,
"properties": {
- "apiKey": {
- "type": "binary"
+ "flapping": {
+ "properties": {}
}
}
},
- "synthetics-param": {
- "dynamic": false,
- "properties": {}
- },
- "infrastructure-ui-source": {
- "dynamic": false,
- "properties": {}
- },
- "inventory-view": {
- "dynamic": false,
- "properties": {}
- },
- "metrics-explorer-view": {
- "dynamic": false,
- "properties": {}
- },
- "upgrade-assistant-reindex-operation": {
- "dynamic": false,
+ "sample-data-telemetry": {
"properties": {
- "indexName": {
- "type": "keyword"
+ "installCount": {
+ "type": "long"
},
- "status": {
- "type": "integer"
+ "unInstallCount": {
+ "type": "long"
}
}
},
- "upgrade-assistant-ml-upgrade-operation": {
+ "search": {
"dynamic": false,
"properties": {
- "snapshotId": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
+ "description": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
}
}
},
- "monitoring-telemetry": {
+ "search-session": {
+ "dynamic": false,
"properties": {
- "reportedClusterUuids": {
+ "created": {
+ "type": "date"
+ },
+ "realmName": {
+ "type": "keyword"
+ },
+ "realmType": {
+ "type": "keyword"
+ },
+ "sessionId": {
+ "type": "keyword"
+ },
+ "username": {
"type": "keyword"
}
}
},
- "enterprise_search_telemetry": {
- "dynamic": false,
- "properties": {}
- },
- "app_search_telemetry": {
+ "search-telemetry": {
"dynamic": false,
"properties": {}
},
- "workplace_search_telemetry": {
+ "security-rule": {
"dynamic": false,
- "properties": {}
- },
- "siem-ui-timeline-note": {
"properties": {
- "eventId": {
+ "rule_id": {
"type": "keyword"
},
- "note": {
- "type": "text"
- },
- "created": {
- "type": "date"
- },
- "createdBy": {
- "type": "text"
- },
- "updated": {
- "type": "date"
- },
- "updatedBy": {
- "type": "text"
+ "version": {
+ "type": "long"
}
}
},
- "siem-ui-timeline-pinned-event": {
+ "security-solution-signals-migration": {
+ "dynamic": false,
"properties": {
- "eventId": {
+ "sourceIndex": {
"type": "keyword"
},
- "created": {
- "type": "date"
- },
- "createdBy": {
- "type": "text"
- },
"updated": {
"type": "date"
},
- "updatedBy": {
- "type": "text"
+ "version": {
+ "type": "long"
}
}
},
"siem-detection-engine-rule-actions": {
"properties": {
- "alertThrottle": {
- "type": "keyword"
- },
- "ruleAlertId": {
- "type": "keyword"
- },
- "ruleThrottle": {
- "type": "keyword"
- },
"actions": {
"properties": {
"actionRef": {
"type": "keyword"
},
- "group": {
+ "action_type_id": {
"type": "keyword"
},
- "id": {
+ "group": {
"type": "keyword"
},
- "action_type_id": {
+ "id": {
"type": "keyword"
},
"params": {
@@ -2805,17 +2539,15 @@
"properties": {}
}
}
- }
- }
- },
- "security-rule": {
- "dynamic": false,
- "properties": {
- "rule_id": {
+ },
+ "alertThrottle": {
"type": "keyword"
},
- "version": {
- "type": "long"
+ "ruleAlertId": {
+ "type": "keyword"
+ },
+ "ruleThrottle": {
+ "type": "keyword"
}
}
},
@@ -2838,10 +2570,10 @@
"example": {
"type": "text"
},
- "indexes": {
+ "id": {
"type": "keyword"
},
- "id": {
+ "indexes": {
"type": "keyword"
},
"name": {
@@ -2858,13 +2590,54 @@
}
}
},
+ "created": {
+ "type": "date"
+ },
+ "createdBy": {
+ "type": "text"
+ },
"dataProviders": {
"properties": {
- "id": {
- "type": "keyword"
- },
- "name": {
- "type": "text"
+ "and": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded": {
+ "type": "boolean"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "kqlQuery": {
+ "type": "text"
+ },
+ "name": {
+ "type": "text"
+ },
+ "queryMatch": {
+ "properties": {
+ "displayField": {
+ "type": "text"
+ },
+ "displayValue": {
+ "type": "text"
+ },
+ "field": {
+ "type": "text"
+ },
+ "operator": {
+ "type": "text"
+ },
+ "value": {
+ "type": "text"
+ }
+ }
+ },
+ "type": {
+ "type": "text"
+ }
+ }
},
"enabled": {
"type": "boolean"
@@ -2872,71 +2645,46 @@
"excluded": {
"type": "boolean"
},
+ "id": {
+ "type": "keyword"
+ },
"kqlQuery": {
"type": "text"
},
- "type": {
+ "name": {
"type": "text"
},
"queryMatch": {
"properties": {
- "field": {
- "type": "text"
- },
"displayField": {
"type": "text"
},
- "value": {
- "type": "text"
- },
"displayValue": {
"type": "text"
},
- "operator": {
- "type": "text"
- }
- }
- },
- "and": {
- "properties": {
- "id": {
- "type": "keyword"
- },
- "name": {
+ "field": {
"type": "text"
},
- "enabled": {
- "type": "boolean"
- },
- "excluded": {
- "type": "boolean"
- },
- "kqlQuery": {
+ "operator": {
"type": "text"
},
- "type": {
+ "value": {
"type": "text"
- },
- "queryMatch": {
- "properties": {
- "field": {
- "type": "text"
- },
- "displayField": {
- "type": "text"
- },
- "value": {
- "type": "text"
- },
- "displayValue": {
- "type": "text"
- },
- "operator": {
- "type": "text"
- }
- }
}
}
+ },
+ "type": {
+ "type": "text"
+ }
+ }
+ },
+ "dateRange": {
+ "properties": {
+ "end": {
+ "type": "date"
+ },
+ "start": {
+ "type": "date"
}
}
},
@@ -2948,16 +2696,16 @@
"eventCategoryField": {
"type": "text"
},
- "tiebreakerField": {
+ "query": {
"type": "text"
},
- "timestampField": {
+ "size": {
"type": "text"
},
- "query": {
+ "tiebreakerField": {
"type": "text"
},
- "size": {
+ "timestampField": {
"type": "text"
}
}
@@ -2970,22 +2718,28 @@
},
"favorite": {
"properties": {
- "keySearch": {
- "type": "text"
+ "favoriteDate": {
+ "type": "date"
},
"fullName": {
"type": "text"
},
- "userName": {
+ "keySearch": {
"type": "text"
},
- "favoriteDate": {
- "type": "date"
+ "userName": {
+ "type": "text"
}
}
},
"filters": {
"properties": {
+ "exists": {
+ "type": "text"
+ },
+ "match_all": {
+ "type": "text"
+ },
"meta": {
"properties": {
"alias": {
@@ -3015,23 +2769,17 @@
"params": {
"type": "text"
},
+ "relation": {
+ "type": "keyword"
+ },
"type": {
"type": "keyword"
},
"value": {
"type": "text"
- },
- "relation": {
- "type": "keyword"
}
}
},
- "exists": {
- "type": "text"
- },
- "match_all": {
- "type": "text"
- },
"missing": {
"type": "text"
},
@@ -3058,186 +2806,438 @@
"properties": {
"kuery": {
"properties": {
- "kind": {
- "type": "keyword"
- },
"expression": {
"type": "text"
+ },
+ "kind": {
+ "type": "keyword"
}
}
},
"serializedQuery": {
"type": "text"
}
- }
+ }
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "text"
+ },
+ "sort": {
+ "dynamic": false,
+ "properties": {
+ "columnId": {
+ "type": "keyword"
+ },
+ "columnType": {
+ "type": "keyword"
+ },
+ "sortDirection": {
+ "type": "keyword"
+ }
+ }
+ },
+ "status": {
+ "type": "keyword"
+ },
+ "templateTimelineId": {
+ "type": "text"
+ },
+ "templateTimelineVersion": {
+ "type": "integer"
+ },
+ "timelineType": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "updated": {
+ "type": "date"
+ },
+ "updatedBy": {
+ "type": "text"
+ }
+ }
+ },
+ "siem-ui-timeline-note": {
+ "properties": {
+ "created": {
+ "type": "date"
+ },
+ "createdBy": {
+ "type": "text"
+ },
+ "eventId": {
+ "type": "keyword"
+ },
+ "note": {
+ "type": "text"
+ },
+ "updated": {
+ "type": "date"
+ },
+ "updatedBy": {
+ "type": "text"
+ }
+ }
+ },
+ "siem-ui-timeline-pinned-event": {
+ "properties": {
+ "created": {
+ "type": "date"
+ },
+ "createdBy": {
+ "type": "text"
+ },
+ "eventId": {
+ "type": "keyword"
+ },
+ "updated": {
+ "type": "date"
+ },
+ "updatedBy": {
+ "type": "text"
+ }
+ }
+ },
+ "slo": {
+ "dynamic": false,
+ "properties": {
+ "budgetingMethod": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "indicator": {
+ "properties": {
+ "params": {
+ "type": "flattened"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "name": {
+ "type": "text"
+ },
+ "tags": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "long"
+ }
+ }
+ },
+ "space": {
+ "dynamic": false,
+ "properties": {
+ "name": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "spaces-usage-stats": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "synthetics-monitor": {
+ "dynamic": false,
+ "properties": {
+ "alert": {
+ "properties": {
+ "status": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ },
+ "tls": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "custom_heartbeat_id": {
+ "type": "keyword"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "hash": {
+ "type": "keyword"
+ },
+ "hosts": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "journey_id": {
+ "type": "keyword"
+ },
+ "locations": {
+ "properties": {
+ "id": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 256,
+ "type": "keyword"
+ },
+ "label": {
+ "type": "text"
}
}
},
- "title": {
- "type": "text"
- },
- "templateTimelineId": {
+ "name": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ }
+ },
"type": "text"
},
- "templateTimelineVersion": {
- "type": "integer"
+ "origin": {
+ "type": "keyword"
},
- "timelineType": {
+ "project_id": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
"type": "keyword"
},
- "dateRange": {
+ "schedule": {
"properties": {
- "start": {
- "type": "date"
- },
- "end": {
- "type": "date"
+ "number": {
+ "type": "integer"
}
}
},
- "sort": {
- "dynamic": false,
+ "tags": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "throttling": {
"properties": {
- "columnId": {
- "type": "keyword"
- },
- "columnType": {
- "type": "keyword"
- },
- "sortDirection": {
+ "label": {
"type": "keyword"
}
}
},
- "status": {
- "type": "keyword"
- },
- "created": {
- "type": "date"
- },
- "createdBy": {
- "type": "text"
- },
- "updated": {
- "type": "date"
- },
- "updatedBy": {
+ "type": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
"type": "text"
},
- "savedSearchId": {
+ "urls": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
"type": "text"
}
}
},
- "endpoint:user-artifact-manifest": {
+ "synthetics-param": {
"dynamic": false,
- "properties": {
- "schemaVersion": {
- "type": "keyword"
- },
- "artifacts": {
- "type": "nested"
- }
- }
+ "properties": {}
},
- "security-solution-signals-migration": {
+ "synthetics-privates-locations": {
"dynamic": false,
+ "properties": {}
+ },
+ "tag": {
"properties": {
- "sourceIndex": {
- "type": "keyword"
+ "color": {
+ "type": "text"
},
- "updated": {
- "type": "date"
+ "description": {
+ "type": "text"
},
- "version": {
- "type": "long"
+ "name": {
+ "type": "text"
}
}
},
- "risk-engine-configuration": {
+ "task": {
"dynamic": false,
"properties": {
- "dataViewId": {
- "type": "keyword"
+ "attempts": {
+ "type": "integer"
},
"enabled": {
"type": "boolean"
},
- "filter": {
- "dynamic": false,
- "properties": {}
- },
- "identifierType": {
+ "ownerId": {
"type": "keyword"
},
- "interval": {
- "type": "keyword"
+ "retryAt": {
+ "type": "date"
},
- "pageSize": {
- "type": "integer"
+ "runAt": {
+ "type": "date"
},
- "range": {
+ "schedule": {
"properties": {
- "start": {
- "type": "keyword"
- },
- "end": {
+ "interval": {
"type": "keyword"
}
}
+ },
+ "scheduledAt": {
+ "type": "date"
+ },
+ "scope": {
+ "type": "keyword"
+ },
+ "status": {
+ "type": "keyword"
+ },
+ "taskType": {
+ "type": "keyword"
}
}
},
- "policy-settings-protection-updates-note": {
+ "telemetry": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "threshold-explorer-view": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "ui-metric": {
"properties": {
- "note": {
- "type": "text",
- "index": false
+ "count": {
+ "type": "integer"
}
}
},
- "apm-telemetry": {
+ "upgrade-assistant-ml-upgrade-operation": {
"dynamic": false,
- "properties": {}
- },
- "apm-server-schema": {
"properties": {
- "schemaJson": {
- "type": "text",
- "index": false
+ "snapshotId": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
}
}
},
- "apm-service-group": {
+ "upgrade-assistant-reindex-operation": {
+ "dynamic": false,
"properties": {
- "groupName": {
+ "indexName": {
"type": "keyword"
},
- "kuery": {
- "type": "text"
+ "status": {
+ "type": "integer"
+ }
+ }
+ },
+ "uptime-dynamic-settings": {
+ "dynamic": false,
+ "properties": {}
+ },
+ "uptime-synthetics-api-key": {
+ "dynamic": false,
+ "properties": {
+ "apiKey": {
+ "type": "binary"
+ }
+ }
+ },
+ "url": {
+ "dynamic": false,
+ "properties": {
+ "accessDate": {
+ "type": "date"
},
- "description": {
- "type": "text"
+ "createDate": {
+ "type": "date"
},
- "color": {
+ "slug": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
"type": "text"
}
}
},
- "apm-custom-dashboards": {
+ "usage-counters": {
+ "dynamic": false,
"properties": {
- "dashboardSavedObjectId": {
+ "domainId": {
"type": "keyword"
- },
- "kuery": {
+ }
+ }
+ },
+ "visualization": {
+ "dynamic": false,
+ "properties": {
+ "description": {
"type": "text"
},
- "serviceEnvironmentFilterEnabled": {
- "type": "boolean"
+ "kibanaSavedObjectMeta": {
+ "properties": {}
},
- "serviceNameFilterEnabled": {
- "type": "boolean"
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
}
}
+ },
+ "workplace_search_telemetry": {
+ "dynamic": false,
+ "properties": {}
}
}
diff --git a/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts b/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts
index 5632f3c479d18..30ff02a624a7f 100644
--- a/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts
+++ b/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts
@@ -10,13 +10,14 @@ import Fsp from 'fs/promises';
import Path from 'path';
import type { SavedObjectsTypeMappingDefinitions } from '@kbn/core-saved-objects-base-server-internal';
+import { prettyPrintAndSortKeys } from '@kbn/utils';
-export const CURRENT_MAPPINGS_FILE = Path.resolve(__dirname, '../../current_mappings.json');
+export const CURRENT_MAPPINGS_FILE_PATH = Path.resolve(__dirname, '../../current_mappings.json');
export async function readCurrentMappings(): Promise {
let currentMappingsJson;
try {
- currentMappingsJson = await Fsp.readFile(CURRENT_MAPPINGS_FILE, 'utf8');
+ currentMappingsJson = await Fsp.readFile(CURRENT_MAPPINGS_FILE_PATH, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
return {};
@@ -28,6 +29,10 @@ export async function readCurrentMappings(): Promise => {
};
export const writeCurrentFields = async (fieldMap: FieldListMap) => {
- await writeFile(CURRENT_FIELDS_FILE_PATH, JSON.stringify(fieldMap, null, 2) + '\n', 'utf-8');
+ await writeFile(CURRENT_FIELDS_FILE_PATH, prettyPrintAndSortKeys(fieldMap) + '\n', 'utf-8');
};
diff --git a/packages/kbn-check-mappings-update-cli/tsconfig.json b/packages/kbn-check-mappings-update-cli/tsconfig.json
index 48973be21d219..b8ae7bad89ebc 100644
--- a/packages/kbn-check-mappings-update-cli/tsconfig.json
+++ b/packages/kbn-check-mappings-update-cli/tsconfig.json
@@ -28,5 +28,6 @@
"@kbn/safer-lodash-set",
"@kbn/tooling-log",
"@kbn/core-saved-objects-server",
+ "@kbn/utils",
]
}
diff --git a/packages/kbn-utils/index.ts b/packages/kbn-utils/index.ts
index d23c71fad55bc..492f6d321ef0e 100644
--- a/packages/kbn-utils/index.ts
+++ b/packages/kbn-utils/index.ts
@@ -5,6 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+export * from './src/json';
export * from './src/path';
export * from './src/streams';
diff --git a/packages/kbn-utils/src/json/index.test.ts b/packages/kbn-utils/src/json/index.test.ts
new file mode 100644
index 0000000000000..d8a81883e3826
--- /dev/null
+++ b/packages/kbn-utils/src/json/index.test.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { prettyPrintAndSortKeys } from '.';
+
+describe('prettyPrintAndSortKeys', () => {
+ it('pretty prints JSON and consistently sorts the keys', () => {
+ const object1 = {
+ zap: { z: 1, b: 2 },
+ foo: 'foo',
+ bar: 'bar',
+ };
+ const object2 = {
+ foo: 'foo',
+ zap: { z: 1, b: 2 },
+ bar: 'bar',
+ };
+ expect(prettyPrintAndSortKeys(object1)).toMatchInlineSnapshot(`
+ "{
+ \\"bar\\": \\"bar\\",
+ \\"foo\\": \\"foo\\",
+ \\"zap\\": {
+ \\"b\\": 2,
+ \\"z\\": 1
+ }
+ }"
+ `);
+
+ expect(prettyPrintAndSortKeys(object1)).toEqual(prettyPrintAndSortKeys(object2));
+ });
+
+ it('pretty prints and sorts nested objects with arrays', () => {
+ const object = {
+ zap: {
+ a: {
+ b: 1,
+ a: 2,
+ },
+ },
+ foo: 'foo',
+ bar: 'bar',
+ baz: [
+ {
+ c: 2,
+ b: 1,
+ },
+ {
+ c: {
+ b: 1,
+ a: 2,
+ },
+ },
+ ],
+ };
+ expect(prettyPrintAndSortKeys(object)).toMatchInlineSnapshot(`
+ "{
+ \\"bar\\": \\"bar\\",
+ \\"baz\\": [
+ {
+ \\"b\\": 1,
+ \\"c\\": 2
+ },
+ {
+ \\"c\\": {
+ \\"a\\": 2,
+ \\"b\\": 1
+ }
+ }
+ ],
+ \\"foo\\": \\"foo\\",
+ \\"zap\\": {
+ \\"a\\": {
+ \\"a\\": 2,
+ \\"b\\": 1
+ }
+ }
+ }"
+ `);
+ });
+});
diff --git a/packages/kbn-utils/src/json/index.ts b/packages/kbn-utils/src/json/index.ts
new file mode 100644
index 0000000000000..7a8afd42283ae
--- /dev/null
+++ b/packages/kbn-utils/src/json/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/**
+ * Given a JS object, will return a JSON.stringified result with consistently
+ * sorted keys.
+ */
+export function prettyPrintAndSortKeys(object: object): string {
+ const keys = new Set();
+ JSON.stringify(object, (key, value) => {
+ keys.add(key);
+ return value;
+ });
+ return JSON.stringify(object, Array.from(keys).sort(), 2);
+}
From 95ef111e10a58741cb2e52e804034d1685bf033e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Thu, 8 Feb 2024 16:21:52 +0000
Subject: [PATCH 031/104] [APM] Hide Table search on Service Inventory
(#176504)
Hide the Table search on the Service Inventory page and enable the
feature by default.
---
.../cypress/e2e/service_inventory/service_inventory.cy.ts | 3 ++-
.../components/app/service_inventory/service_list/index.tsx | 1 +
.../apm/public/components/shared/managed_table/index.tsx | 2 +-
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts
index c0c3c032a0e61..5c50e79c145aa 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts
@@ -115,7 +115,8 @@ describe('Service inventory', () => {
});
});
- describe('Table search', () => {
+ // Skipping this until we enable the table search on the Service inventory view
+ describe.skip('Table search', () => {
beforeEach(() => {
cy.updateAdvancedSettings({
'observability:apmEnableTableSearchBar': true,
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
index 08e7a840b5dfb..ba55defaaf4d7 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
@@ -357,6 +357,7 @@ export function ServiceList({
const tableSearchBar: TableSearchBar = useMemo(() => {
return {
+ isEnabled: false,
fieldsToSearch: ['serviceName'],
maxCountExceeded,
onChangeSearchQuery,
diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx
index 7d6307d32ffb8..ae14f63f8d72b 100644
--- a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx
@@ -111,7 +111,7 @@ function UnoptimizedManagedTable(props: {
const { core } = useApmPluginContext();
const isTableSearchBarEnabled = core.uiSettings.get(
apmEnableTableSearchBar,
- false
+ true
);
const {
From 5aa66564da5ff7af33107e2b6e5ca81665b51d30 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 8 Feb 2024 16:26:53 +0000
Subject: [PATCH 032/104] skip flaky suite (#163263)
---
x-pack/plugins/cases/public/components/app/routes.test.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/cases/public/components/app/routes.test.tsx b/x-pack/plugins/cases/public/components/app/routes.test.tsx
index 9c435e2b163ba..1127eb059fc8b 100644
--- a/x-pack/plugins/cases/public/components/app/routes.test.tsx
+++ b/x-pack/plugins/cases/public/components/app/routes.test.tsx
@@ -60,7 +60,8 @@ describe('Cases routes', () => {
});
});
- describe('Case view', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/163263
+ describe.skip('Case view', () => {
it.each(getCaseViewPaths())(
'navigates to the cases view page for path: %s',
async (path: string) => {
From fc58a0d3a71dd946fb24a75050930030c002d2a4 Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Thu, 8 Feb 2024 17:27:24 +0100
Subject: [PATCH 033/104] [Obs AI Assistant] Improve recall speed (#176428)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Improves recall speed by outputting as CSV with zero-indexed document
"ids". Previously, it was a JSON object, with the real document ids.
This causes the LLM to "think" for longer, for whatever reason. I didn't
actually see a difference in completion speed, but emitting the first
value took significantly less time when using the CSV output. I also
tried sending a single document per request using the old format, and
while that certainly improves things, the slowest request becomes the
bottleneck. These are results from about 10 tries per strategy (I'd love
to see others reproduce at least the `batch` vs `csv` strategy results):
`batch`: 24.7s
`chunk`: 10s
`csv`: 4.9s
---------
Co-authored-by: Søren Louv-Jansen
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../server/functions/recall.ts | 106 ++++++++----------
.../server/service/client/index.ts | 62 +++++++---
2 files changed, 91 insertions(+), 77 deletions(-)
diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts b/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts
index ee0fae1f91ed1..909a823286cc6 100644
--- a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts
+++ b/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts
@@ -9,7 +9,7 @@ import { decodeOrThrow, jsonRt } from '@kbn/io-ts-utils';
import type { Serializable } from '@kbn/utility-types';
import dedent from 'dedent';
import * as t from 'io-ts';
-import { last, omit } from 'lodash';
+import { compact, last, omit } from 'lodash';
import { lastValueFrom } from 'rxjs';
import { FunctionRegistrationParameters } from '.';
import { MessageRole, type Message } from '../../common/types';
@@ -88,12 +88,17 @@ export function registerRecallFunction({
messages.filter((message) => message.message.role === MessageRole.User)
);
+ const nonEmptyQueries = compact(queries);
+
+ const queriesOrUserPrompt = nonEmptyQueries.length
+ ? nonEmptyQueries
+ : compact([userMessage?.message.content]);
+
const suggestions = await retrieveSuggestions({
userMessage,
client,
- signal,
categories,
- queries,
+ queries: queriesOrUserPrompt,
});
if (suggestions.length === 0) {
@@ -104,9 +109,8 @@ export function registerRecallFunction({
const relevantDocuments = await scoreSuggestions({
suggestions,
- systemMessage,
- userMessage,
- queries,
+ queries: queriesOrUserPrompt,
+ messages,
client,
connectorId,
signal,
@@ -121,25 +125,17 @@ export function registerRecallFunction({
}
async function retrieveSuggestions({
- userMessage,
queries,
client,
categories,
- signal,
}: {
userMessage?: Message;
queries: string[];
client: ObservabilityAIAssistantClient;
categories: Array<'apm' | 'lens'>;
- signal: AbortSignal;
}) {
- const queriesWithUserPrompt =
- userMessage && userMessage.message.content
- ? [userMessage.message.content, ...queries]
- : queries;
-
const recallResponse = await client.recall({
- queries: queriesWithUserPrompt,
+ queries,
categories,
});
@@ -156,18 +152,12 @@ const scoreFunctionRequestRt = t.type({
});
const scoreFunctionArgumentsRt = t.type({
- scores: t.array(
- t.type({
- id: t.string,
- score: t.number,
- })
- ),
+ scores: t.string,
});
async function scoreSuggestions({
suggestions,
- systemMessage,
- userMessage,
+ messages,
queries,
client,
connectorId,
@@ -175,35 +165,31 @@ async function scoreSuggestions({
resources,
}: {
suggestions: Awaited>;
- systemMessage: Message;
- userMessage?: Message;
+ messages: Message[];
queries: string[];
client: ObservabilityAIAssistantClient;
connectorId: string;
signal: AbortSignal;
resources: RespondFunctionResources;
}) {
- resources.logger.debug(`Suggestions: ${JSON.stringify(suggestions, null, 2)}`);
+ const indexedSuggestions = suggestions.map((suggestion, index) => ({ ...suggestion, id: index }));
- const systemMessageExtension =
- dedent(`You have the function called score available to help you inform the user about how relevant you think a given document is to the conversation.
- Please give a score between 1 and 7, fractions are allowed.
- A higher score means it is more relevant.`);
- const extendedSystemMessage = {
- ...systemMessage,
- message: {
- ...systemMessage.message,
- content: `${systemMessage.message.content}\n\n${systemMessageExtension}`,
- },
- };
+ const newUserMessageContent =
+ dedent(`Given the following question, score the documents that are relevant to the question. on a scale from 0 to 7,
+ 0 being completely relevant, and 7 being extremely relevant. Information is relevant to the question if it helps in
+ answering the question. Judge it according to the following criteria:
- const userMessageOrQueries =
- userMessage && userMessage.message.content ? userMessage.message.content : queries.join(',');
+ - The document is relevant to the question, and the rest of the conversation
+ - The document has information relevant to the question that is not mentioned,
+ or more detailed than what is available in the conversation
+ - The document has a high amount of information relevant to the question compared to other documents
+ - The document contains new information not mentioned before in the conversation
- const newUserMessageContent =
- dedent(`Given the question "${userMessageOrQueries}", can you give me a score for how relevant the following documents are?
+ Question:
+ ${queries.join('\n')}
- ${JSON.stringify(suggestions, null, 2)}`);
+ Documents:
+ ${JSON.stringify(indexedSuggestions, null, 2)}`);
const newUserMessage: Message = {
'@timestamp': new Date().toISOString(),
@@ -222,22 +208,13 @@ async function scoreSuggestions({
additionalProperties: false,
properties: {
scores: {
- description: 'The document IDs and their scores',
- type: 'array',
- items: {
- type: 'object',
- additionalProperties: false,
- properties: {
- id: {
- description: 'The ID of the document',
- type: 'string',
- },
- score: {
- description: 'The score for the document',
- type: 'number',
- },
- },
- },
+ description: `The document IDs and their scores, as CSV. Example:
+
+ my_id,7
+ my_other_id,3
+ my_third_id,4
+ `,
+ type: 'string',
},
},
required: ['score'],
@@ -249,7 +226,7 @@ async function scoreSuggestions({
(
await client.chat('score_suggestions', {
connectorId,
- messages: [extendedSystemMessage, newUserMessage],
+ messages: [...messages.slice(0, -1), newUserMessage],
functions: [scoreFunction],
functionCall: 'score',
signal,
@@ -257,11 +234,18 @@ async function scoreSuggestions({
).pipe(concatenateChatCompletionChunks())
);
const scoreFunctionRequest = decodeOrThrow(scoreFunctionRequestRt)(response);
- const { scores } = decodeOrThrow(jsonRt.pipe(scoreFunctionArgumentsRt))(
+ const { scores: scoresAsString } = decodeOrThrow(jsonRt.pipe(scoreFunctionArgumentsRt))(
scoreFunctionRequest.message.function_call.arguments
);
- resources.logger.debug(`Scores: ${JSON.stringify(scores, null, 2)}`);
+ const scores = scoresAsString.split('\n').map((line) => {
+ const [index, score] = line
+ .split(',')
+ .map((value) => value.trim())
+ .map(Number);
+
+ return { id: suggestions[index].id, score };
+ });
if (scores.length === 0) {
return [];
diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts
index f3ab3e917979b..afd34aa8ea966 100644
--- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts
+++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts
@@ -14,7 +14,15 @@ import apm from 'elastic-apm-node';
import { decode, encode } from 'gpt-tokenizer';
import { compact, isEmpty, last, merge, noop, omit, pick, take } from 'lodash';
import type OpenAI from 'openai';
-import { filter, isObservable, lastValueFrom, Observable, shareReplay, toArray } from 'rxjs';
+import {
+ filter,
+ firstValueFrom,
+ isObservable,
+ lastValueFrom,
+ Observable,
+ shareReplay,
+ toArray,
+} from 'rxjs';
import { Readable } from 'stream';
import { v4 } from 'uuid';
import {
@@ -455,6 +463,8 @@ export class ObservabilityAIAssistantClient {
): Promise> => {
const span = apm.startSpan(`chat ${name}`);
+ const spanId = (span?.ids['span.id'] || '').substring(0, 6);
+
const messagesForOpenAI: Array<
Omit & {
role: MessageRole;
@@ -490,6 +500,8 @@ export class ObservabilityAIAssistantClient {
this.dependencies.logger.debug(`Sending conversation to connector`);
this.dependencies.logger.trace(JSON.stringify(request, null, 2));
+ const now = performance.now();
+
const executeResult = await this.dependencies.actionsClient.execute({
actionId: connectorId,
params: {
@@ -501,7 +513,11 @@ export class ObservabilityAIAssistantClient {
},
});
- this.dependencies.logger.debug(`Received action client response: ${executeResult.status}`);
+ this.dependencies.logger.debug(
+ `Received action client response: ${executeResult.status} (took: ${Math.round(
+ performance.now() - now
+ )}ms)${spanId ? ` (${spanId})` : ''}`
+ );
if (executeResult.status === 'error' && executeResult?.serviceMessage) {
const tokenLimitRegex =
@@ -524,20 +540,34 @@ export class ObservabilityAIAssistantClient {
const observable = streamIntoObservable(response).pipe(processOpenAiStream(), shareReplay());
- if (span) {
- lastValueFrom(observable)
- .then(
- () => {
- span.setOutcome('success');
- },
- () => {
- span.setOutcome('failure');
- }
- )
- .finally(() => {
- span.end();
- });
- }
+ firstValueFrom(observable)
+ .catch(noop)
+ .finally(() => {
+ this.dependencies.logger.debug(
+ `Received first value after ${Math.round(performance.now() - now)}ms${
+ spanId ? ` (${spanId})` : ''
+ }`
+ );
+ });
+
+ lastValueFrom(observable)
+ .then(
+ () => {
+ span?.setOutcome('success');
+ },
+ () => {
+ span?.setOutcome('failure');
+ }
+ )
+ .finally(() => {
+ this.dependencies.logger.debug(
+ `Completed response in ${Math.round(performance.now() - now)}ms${
+ spanId ? ` (${spanId})` : ''
+ }`
+ );
+
+ span?.end();
+ });
return observable;
};
From 03b57332b88a61926855af09e84cf82519782e6c Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 8 Feb 2024 16:28:24 +0000
Subject: [PATCH 034/104] skip flaky suite (#175229)
---
x-pack/plugins/cases/public/components/app/routes.test.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/cases/public/components/app/routes.test.tsx b/x-pack/plugins/cases/public/components/app/routes.test.tsx
index 1127eb059fc8b..71eeb495a1d8a 100644
--- a/x-pack/plugins/cases/public/components/app/routes.test.tsx
+++ b/x-pack/plugins/cases/public/components/app/routes.test.tsx
@@ -85,7 +85,8 @@ describe('Cases routes', () => {
);
});
- describe('Create case', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/175229
+ describe.skip('Create case', () => {
it('navigates to the create case page', () => {
renderWithRouter(['/cases/create']);
expect(screen.getByText('Create case')).toBeInTheDocument();
From a81a8cacdba972408cec14824fc9f1163489d19d Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 8 Feb 2024 16:28:49 +0000
Subject: [PATCH 035/104] skip flaky suite (#175230)
---
x-pack/plugins/cases/public/components/app/routes.test.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/plugins/cases/public/components/app/routes.test.tsx b/x-pack/plugins/cases/public/components/app/routes.test.tsx
index 71eeb495a1d8a..4b94821d83bd3 100644
--- a/x-pack/plugins/cases/public/components/app/routes.test.tsx
+++ b/x-pack/plugins/cases/public/components/app/routes.test.tsx
@@ -86,6 +86,7 @@ describe('Cases routes', () => {
});
// FLAKY: https://github.com/elastic/kibana/issues/175229
+ // FLAKY: https://github.com/elastic/kibana/issues/175230
describe.skip('Create case', () => {
it('navigates to the create case page', () => {
renderWithRouter(['/cases/create']);
From 3ff578287919b731265090a8b9e68d5b8d883cf7 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 8 Feb 2024 16:29:14 +0000
Subject: [PATCH 036/104] skip flaky suite (#175231)
---
x-pack/plugins/cases/public/components/app/routes.test.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/cases/public/components/app/routes.test.tsx b/x-pack/plugins/cases/public/components/app/routes.test.tsx
index 4b94821d83bd3..0f51642670031 100644
--- a/x-pack/plugins/cases/public/components/app/routes.test.tsx
+++ b/x-pack/plugins/cases/public/components/app/routes.test.tsx
@@ -99,7 +99,8 @@ describe('Cases routes', () => {
});
});
- describe('Cases settings', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/175231
+ describe.skip('Cases settings', () => {
it('navigates to the cases settings page', () => {
renderWithRouter(['/cases/configure']);
expect(screen.getByText('Settings')).toBeInTheDocument();
From 6a9e18e131e8deb2b413068f1cafa947dc19cd1b Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 8 Feb 2024 16:29:36 +0000
Subject: [PATCH 037/104] skip flaky suite (#175232)
---
x-pack/plugins/cases/public/components/app/routes.test.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/plugins/cases/public/components/app/routes.test.tsx b/x-pack/plugins/cases/public/components/app/routes.test.tsx
index 0f51642670031..91b4b1ef5227f 100644
--- a/x-pack/plugins/cases/public/components/app/routes.test.tsx
+++ b/x-pack/plugins/cases/public/components/app/routes.test.tsx
@@ -100,6 +100,7 @@ describe('Cases routes', () => {
});
// FLAKY: https://github.com/elastic/kibana/issues/175231
+ // FLAKY: https://github.com/elastic/kibana/issues/175232
describe.skip('Cases settings', () => {
it('navigates to the cases settings page', () => {
renderWithRouter(['/cases/configure']);
From df273cd23e1ed78170b44d37418fbe65c2ce80d5 Mon Sep 17 00:00:00 2001
From: Ash <1849116+ashokaditya@users.noreply.github.com>
Date: Thu, 8 Feb 2024 17:49:55 +0100
Subject: [PATCH 038/104] [Security Solution][Endpoint] Gate responder action
item for sentinel one alert with feature flag (#176405)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Shows responder action item enabled for SentinelOne alerts if feature
flag is enabled.
**with `responseActionsSentinelOneV1Enabled` enabled**
Follow up of elastic/kibana/pull/173927
### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
---
.../use_responder_action_data.ts | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts
index 62772fa029e66..cfa004b7ce6b2 100644
--- a/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts
@@ -7,6 +7,7 @@
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
+import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check';
import type { ThirdPartyAgentInfo } from '../../../../common/types';
import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants';
@@ -74,17 +75,22 @@ export const useResponderActionData = ({
tooltip: ReactNode;
} => {
const isEndpointHost = agentType === 'endpoint';
+ const isSentinelOneHost = agentType === 'sentinel_one';
const showResponseActionsConsole = useWithShowResponder();
+ const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
+ 'responseActionsSentinelOneV1Enabled'
+ );
const {
data: hostInfo,
isFetching,
error,
- } = useGetEndpointDetails(endpointId, { enabled: Boolean(endpointId) });
+ } = useGetEndpointDetails(endpointId, { enabled: Boolean(endpointId && isEndpointHost) });
const [isDisabled, tooltip]: [disabled: boolean, tooltip: ReactNode] = useMemo(() => {
+ // v8.13 disabled for third-party agent alerts if the feature flag is not enabled
if (!isEndpointHost) {
- return [false, undefined];
+ return [isSentinelOneHost ? !isSentinelOneV1Enabled : true, undefined];
}
// Still loading host info
@@ -114,7 +120,14 @@ export const useResponderActionData = ({
}
return [false, undefined];
- }, [isEndpointHost, isFetching, error, hostInfo?.host_status]);
+ }, [
+ isEndpointHost,
+ isSentinelOneHost,
+ isSentinelOneV1Enabled,
+ isFetching,
+ error,
+ hostInfo?.host_status,
+ ]);
const handleResponseActionsClick = useCallback(() => {
if (!isEndpointHost) {
From 3cde1a5613cb862190022f338a211dc23c3f612a Mon Sep 17 00:00:00 2001
From: Elastic Machine
Date: Fri, 9 Feb 2024 03:38:44 +1030
Subject: [PATCH 039/104] [main] Sync bundled packages with Package Storage
(#176510)
Automated by
https://buildkite.com/elastic/package-storage-infra-kibana-discover-release-branches/builds/337
---
fleet_packages.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fleet_packages.json b/fleet_packages.json
index 836e65d0e95aa..bfde442c20d22 100644
--- a/fleet_packages.json
+++ b/fleet_packages.json
@@ -56,6 +56,6 @@
},
{
"name": "security_detection_engine",
- "version": "8.12.3"
+ "version": "8.12.4"
}
]
\ No newline at end of file
From b9ce46958255c17954f29fbb8cfefc65a7078407 Mon Sep 17 00:00:00 2001
From: acrewdson
Date: Thu, 8 Feb 2024 09:37:24 -0800
Subject: [PATCH 040/104] [Search] Use more normative default ports for
Postgres and MS SQL Server (#176020)
## Summary
Related connectors issue:
https://github.com/elastic/connectors/issues/1824
------
We should use more normative default ports in the connectors UI for
Postgres and MS SQL Server. See:
* https://www.postgresql.org/docs/current/app-postgres.html
*
https://learn.microsoft.com/en-us/sql/sql-server/install/configure-the-windows-firewall-to-allow-sql-server-access?view=sql-server-ver16#ports-used-by-the-database-engine
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
Co-authored-by: Ioana Tagirta
---
packages/kbn-search-connectors/types/native_connectors.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/kbn-search-connectors/types/native_connectors.ts b/packages/kbn-search-connectors/types/native_connectors.ts
index 62ac7445e8b06..849e669c4a810 100644
--- a/packages/kbn-search-connectors/types/native_connectors.ts
+++ b/packages/kbn-search-connectors/types/native_connectors.ts
@@ -1793,7 +1793,7 @@ export const NATIVE_CONNECTOR_DEFINITIONS: Record
Date: Thu, 8 Feb 2024 18:39:01 +0100
Subject: [PATCH 041/104] [ES|QL] Canceling the async query from the Lens
inline editing flyout (#176277)
## Summary
adds support for canceling queries in lens side editor
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
---------
Co-authored-by: Stratoula Kalafateli
---
.../src/editor_footer.tsx | 16 +++--
.../src/fetch_fields_from_esql.ts | 11 +++-
packages/kbn-text-based-editor/src/helpers.ts | 11 ++++
.../src/text_based_languages_editor.tsx | 64 +++++++++++++++----
.../query_string_input/query_bar_top_row.tsx | 2 +-
.../index_data_visualizer_esql.tsx | 2 +-
.../shared/edit_on_the_fly/helpers.ts | 18 ++++--
.../lens_configuration_flyout.tsx | 11 ++--
.../expression/esql_query_expression.tsx | 3 +-
9 files changed, 107 insertions(+), 31 deletions(-)
diff --git a/packages/kbn-text-based-editor/src/editor_footer.tsx b/packages/kbn-text-based-editor/src/editor_footer.tsx
index 351b7bbe251c1..210988ac94e42 100644
--- a/packages/kbn-text-based-editor/src/editor_footer.tsx
+++ b/packages/kbn-text-based-editor/src/editor_footer.tsx
@@ -201,6 +201,7 @@ interface EditorFooterProps {
editorIsInline?: boolean;
isSpaceReduced?: boolean;
isLoading?: boolean;
+ allowQueryCancellation?: boolean;
}
export const EditorFooter = memo(function EditorFooter({
@@ -216,6 +217,7 @@ export const EditorFooter = memo(function EditorFooter({
editorIsInline,
isSpaceReduced,
isLoading,
+ allowQueryCancellation,
}: EditorFooterProps) {
const { euiTheme } = useEuiTheme();
const [isErrorPopoverOpen, setIsErrorPopoverOpen] = useState(false);
@@ -333,8 +335,8 @@ export const EditorFooter = memo(function EditorFooter({
size="s"
fill
onClick={runQuery}
- isLoading={isLoading}
- isDisabled={Boolean(disableSubmitAction)}
+ isLoading={isLoading && !allowQueryCancellation}
+ isDisabled={Boolean(disableSubmitAction && !allowQueryCancellation)}
data-test-subj="TextBasedLangEditor-run-query-button"
minWidth={isSpaceReduced ? false : undefined}
>
@@ -345,7 +347,11 @@ export const EditorFooter = memo(function EditorFooter({
justifyContent="spaceBetween"
>
- {isSpaceReduced
+ {allowQueryCancellation && isLoading
+ ? i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.cancel', {
+ defaultMessage: 'Cancel',
+ })
+ : isSpaceReduced
? i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.run', {
defaultMessage: 'Run',
})
@@ -361,7 +367,7 @@ export const EditorFooter = memo(function EditorFooter({
size="xs"
css={css`
border: 1px solid
- ${Boolean(disableSubmitAction)
+ ${Boolean(disableSubmitAction && !allowQueryCancellation)
? euiTheme.colors.disabled
: euiTheme.colors.emptyShade};
padding: 0 ${euiTheme.size.xs};
@@ -370,7 +376,7 @@ export const EditorFooter = memo(function EditorFooter({
border-radius: ${euiTheme.size.xs};
`}
>
- {COMMAND_KEY}⏎
+ {allowQueryCancellation && isLoading ? 'X' : `${COMMAND_KEY}⏎`}
diff --git a/packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts b/packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts
index e1ef2a5d4a8b3..f1013f1a4b329 100644
--- a/packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts
+++ b/packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts
@@ -24,6 +24,7 @@ export function fetchFieldsFromESQL(
query: Query | AggregateQuery,
expressions: ExpressionsStart,
time?: TimeRange,
+ abortController?: AbortController,
dataView?: DataView
) {
return textBasedQueryStateToAstWithValidation({
@@ -33,7 +34,15 @@ export function fetchFieldsFromESQL(
})
.then((ast) => {
if (ast) {
- const execution = expressions.run(ast, null);
+ const executionContract = expressions.execute(ast, null);
+
+ if (abortController) {
+ abortController.signal.onabort = () => {
+ executionContract.cancel();
+ };
+ }
+
+ const execution = executionContract.getData();
let finalData: Datatable;
let error: string | undefined;
execution.pipe(pluck('result')).subscribe((resp) => {
diff --git a/packages/kbn-text-based-editor/src/helpers.ts b/packages/kbn-text-based-editor/src/helpers.ts
index 88df8ef6a75e4..dffddc9514449 100644
--- a/packages/kbn-text-based-editor/src/helpers.ts
+++ b/packages/kbn-text-based-editor/src/helpers.ts
@@ -116,6 +116,17 @@ export const parseErrors = (errors: Error[], code: string): MonacoMessage[] => {
endLineNumber: Number(lineNumber),
severity: monaco.MarkerSeverity.Error,
};
+ } else if (error.message.includes('expression was aborted')) {
+ return {
+ message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.aborted', {
+ defaultMessage: 'Request was aborted',
+ }),
+ startColumn: 1,
+ startLineNumber: 1,
+ endColumn: 10,
+ endLineNumber: 1,
+ severity: monaco.MarkerSeverity.Warning,
+ };
} else {
// unknown error message
return {
diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
index 7bdfce427bc21..241679734e248 100644
--- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
+++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
@@ -71,7 +71,10 @@ export interface TextBasedLanguagesEditorProps {
/** Callback running everytime the query changes */
onTextLangQueryChange: (query: AggregateQuery) => void;
/** Callback running when the user submits the query */
- onTextLangQuerySubmit: (query?: AggregateQuery) => void;
+ onTextLangQuerySubmit: (
+ query?: AggregateQuery,
+ abortController?: AbortController
+ ) => Promise;
/** Can be used to expand/minimize the editor */
expandCodeEditor: (status: boolean) => void;
/** If it is true, the editor initializes with height EDITOR_INITIAL_HEIGHT_EXPANDED */
@@ -105,6 +108,9 @@ export interface TextBasedLanguagesEditorProps {
editorIsInline?: boolean;
/** Disables the submit query action*/
disableSubmitAction?: boolean;
+
+ /** when set to true enables query cancellation **/
+ allowQueryCancellation?: boolean;
}
interface TextBasedEditorDeps {
@@ -158,6 +164,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
editorIsInline,
disableSubmitAction,
dataTestSubj,
+ allowQueryCancellation,
}: TextBasedLanguagesEditorProps) {
const { euiTheme } = useEuiTheme();
const language = getAggregateQueryMode(query);
@@ -176,7 +183,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const [showLineNumbers, setShowLineNumbers] = useState(isCodeEditorExpanded);
const [isCompactFocused, setIsCompactFocused] = useState(isCodeEditorExpanded);
const [isCodeEditorExpandedFocused, setIsCodeEditorExpandedFocused] = useState(false);
-
+ const [isQueryLoading, setIsQueryLoading] = useState(true);
+ const [abortController, setAbortController] = useState(new AbortController());
const [editorMessages, setEditorMessages] = useState<{
errors: MonacoMessage[];
warnings: MonacoMessage[];
@@ -186,12 +194,25 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
});
const onQuerySubmit = useCallback(() => {
- const currentValue = editor1.current?.getValue();
- if (currentValue != null) {
- setCodeStateOnSubmission(currentValue);
+ if (isQueryLoading && allowQueryCancellation) {
+ abortController?.abort();
+ setIsQueryLoading(false);
+ } else {
+ setIsQueryLoading(true);
+ const abc = new AbortController();
+ setAbortController(abc);
+
+ const currentValue = editor1.current?.getValue();
+ if (currentValue != null) {
+ setCodeStateOnSubmission(currentValue);
+ }
+ onTextLangQuerySubmit({ [language]: currentValue } as AggregateQuery, abc);
}
- onTextLangQuerySubmit({ [language]: currentValue } as AggregateQuery);
- }, [language, onTextLangQuerySubmit]);
+ }, [language, onTextLangQuerySubmit, abortController, isQueryLoading, allowQueryCancellation]);
+
+ useEffect(() => {
+ if (!isLoading) setIsQueryLoading(false);
+ }, [isLoading]);
const [documentationSections, setDocumentationSections] =
useState();
@@ -311,12 +332,13 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const { cache: esqlFieldsCache, memoizedFieldsFromESQL } = useMemo(() => {
// need to store the timing of the first request so we can atomically clear the cache per query
const fn = memoize(
- (...args: [{ esql: string }, ExpressionsStart]) => ({
+ (...args: [{ esql: string }, ExpressionsStart, undefined, AbortController?]) => ({
timestamp: Date.now(),
result: fetchFieldsFromESQL(...args),
}),
({ esql }) => esql
);
+
return { cache: fn.cache, memoizedFieldsFromESQL: fn };
}, []);
@@ -334,7 +356,12 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
// Check if there's a stale entry and clear it
clearCacheWhenOld(esqlFieldsCache, esqlQuery.esql);
try {
- const table = await memoizedFieldsFromESQL(esqlQuery, expressions).result;
+ const table = await memoizedFieldsFromESQL(
+ esqlQuery,
+ expressions,
+ undefined,
+ abortController
+ ).result;
return table?.columns.map((c) => ({ name: c.name, type: c.meta.type })) || [];
} catch (e) {
// no action yet
@@ -352,7 +379,14 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
return policies.map(({ type, query: policyQuery, ...rest }) => rest);
},
}),
- [dataViews, expressions, indexManagementApiService, esqlFieldsCache, memoizedFieldsFromESQL]
+ [
+ dataViews,
+ expressions,
+ indexManagementApiService,
+ esqlFieldsCache,
+ memoizedFieldsFromESQL,
+ abortController,
+ ]
);
const queryValidation = useCallback(
@@ -867,7 +901,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
disableSubmitAction={disableSubmitAction}
hideRunQueryText={hideRunQueryText}
isSpaceReduced={isSpaceReduced}
- isLoading={isLoading}
+ isLoading={isQueryLoading}
+ allowQueryCancellation={allowQueryCancellation}
/>
)}
@@ -954,13 +989,16 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
lines={lines}
containerCSS={styles.bottomContainer}
onErrorClick={onErrorClick}
- runQuery={onQuerySubmit}
+ runQuery={() => {
+ onQuerySubmit();
+ }}
detectTimestamp={detectTimestamp}
hideRunQueryText={hideRunQueryText}
editorIsInline={editorIsInline}
disableSubmitAction={disableSubmitAction}
isSpaceReduced={isSpaceReduced}
- isLoading={isLoading}
+ isLoading={isQueryLoading}
+ allowQueryCancellation={allowQueryCancellation}
{...editorMessages}
/>
)}
diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx
index 0e4020f5c70fa..dd9fb37258ed3 100644
--- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx
+++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx
@@ -717,7 +717,7 @@ export const QueryBarTopRow = React.memo(
errors={props.textBasedLanguageModeErrors}
warning={props.textBasedLanguageModeWarning}
detectTimestamp={detectTimestamp}
- onTextLangQuerySubmit={() =>
+ onTextLangQuerySubmit={async () =>
onSubmit({
query: queryRef.current,
dateRange: dateRangeRef.current,
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx
index 0faa236e30c6f..2f91dce01b456 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx
@@ -675,7 +675,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi
// Query that has been typed, but has not submitted with cmd + enter
const [localQuery, setLocalQuery] = useState({ esql: '' });
- const onQueryUpdate = (q?: AggregateQuery) => {
+ const onQueryUpdate = async (q?: AggregateQuery) => {
// When user submits a new query
// resets all current requests and other data
if (cancelOverallStatsRequest) {
diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts
index 475862664c336..803fcbf169935 100644
--- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts
+++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts
@@ -15,7 +15,11 @@ import type { LensPluginStartDependencies } from '../../../plugin';
import type { DatasourceMap, VisualizationMap } from '../../../types';
import { suggestionsApi } from '../../../lens_suggestions_api';
-export const getQueryColumns = async (query: AggregateQuery, deps: LensPluginStartDependencies) => {
+export const getQueryColumns = async (
+ query: AggregateQuery,
+ deps: LensPluginStartDependencies,
+ abortController?: AbortController
+) => {
// Fetching only columns for ES|QL for performance reasons with limit 0
// Important note: ES doesnt return the warnings for 0 limit,
// I am skipping them in favor of performance now
@@ -24,7 +28,12 @@ export const getQueryColumns = async (query: AggregateQuery, deps: LensPluginSta
if ('esql' in performantQuery && performantQuery.esql) {
performantQuery.esql = `${performantQuery.esql} | limit 0`;
}
- const table = await fetchFieldsFromESQL(performantQuery, deps.expressions);
+ const table = await fetchFieldsFromESQL(
+ performantQuery,
+ deps.expressions,
+ undefined,
+ abortController
+ );
return table?.columns;
};
@@ -34,7 +43,8 @@ export const getSuggestions = async (
datasourceMap: DatasourceMap,
visualizationMap: VisualizationMap,
adHocDataViews: DataViewSpec[],
- setErrors: (errors: Error[]) => void
+ setErrors: (errors: Error[]) => void,
+ abortController?: AbortController
) => {
try {
let indexPattern = '';
@@ -55,7 +65,7 @@ export const getSuggestions = async (
if (dataView.fields.getByName('@timestamp')?.type === 'date' && !dataViewSpec) {
dataView.timeFieldName = '@timestamp';
}
- const columns = await getQueryColumns(query, deps);
+ const columns = await getQueryColumns(query, deps, abortController);
const context = {
dataViewSpec: dataView?.toSpec(),
fieldName: '',
diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx
index 834929d4ca2a5..3e2bf4f60aa2b 100644
--- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx
@@ -279,14 +279,15 @@ export function LensEditConfigurationFlyout({
const adHocDataViews = Object.values(attributes.state.adHocDataViews ?? {});
const runQuery = useCallback(
- async (q) => {
+ async (q, abortController) => {
const attrs = await getSuggestions(
q,
startDependencies,
datasourceMap,
visualizationMap,
adHocDataViews,
- setErrors
+ setErrors,
+ abortController
);
if (attrs) {
setCurrentAttributes?.(attrs);
@@ -442,13 +443,13 @@ export function LensEditConfigurationFlyout({
hideMinimizeButton
editorIsInline
hideRunQueryText
- disableSubmitAction={isEqual(query, prevQuery.current)}
- onTextLangQuerySubmit={(q) => {
+ onTextLangQuerySubmit={async (q, a) => {
if (q) {
- runQuery(q);
+ await runQuery(q, a);
}
}}
isDisabled={false}
+ allowQueryCancellation={true}
/>
)}
diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx
index 7e3aa104f5a60..fd9a63f5547e0 100644
--- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx
+++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx
@@ -116,6 +116,7 @@ export const EsqlQueryExpression: React.FC<
from: new Date(now - timeWindow).toISOString(),
to: new Date(now).toISOString(),
},
+ undefined,
// create a data view with the timefield to pass into the query
new DataView({
spec: { timeFieldName: timeField },
@@ -219,7 +220,7 @@ export const EsqlQueryExpression: React.FC<
}, 1000)}
expandCodeEditor={() => true}
isCodeEditorExpanded={true}
- onTextLangQuerySubmit={() => {}}
+ onTextLangQuerySubmit={async () => {}}
detectTimestamp={detectTimestamp}
hideMinimizeButton={true}
hideRunQueryText={true}
From c094f611bbe152f8db1617ab5a11b24546951d45 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 8 Feb 2024 17:49:48 +0000
Subject: [PATCH 042/104] skip flaky suite (#176336)
---
.../cases/public/components/all_cases/severity_filter.test.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx
index 6924cbd13f1c7..8d9f1f948cdc2 100644
--- a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx
@@ -14,7 +14,8 @@ import { screen, waitFor } from '@testing-library/react';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import { SeverityFilter } from './severity_filter';
-describe('Severity form field', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/176336
+describe.skip('Severity form field', () => {
const onChange = jest.fn();
let appMockRender: AppMockRenderer;
const props = {
From 82eb8e3dcbcb90eb9719d5d84fd2146f887c265e Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 8 Feb 2024 17:50:11 +0000
Subject: [PATCH 043/104] skip flaky suite (#176337)
---
.../cases/public/components/all_cases/severity_filter.test.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx
index 8d9f1f948cdc2..28be7c63f22b3 100644
--- a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx
@@ -15,6 +15,7 @@ import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import { SeverityFilter } from './severity_filter';
// FLAKY: https://github.com/elastic/kibana/issues/176336
+// FLAKY: https://github.com/elastic/kibana/issues/176337
describe.skip('Severity form field', () => {
const onChange = jest.fn();
let appMockRender: AppMockRenderer;
From 2d15f36edf76c536094a82f4a8020d66401ca411 Mon Sep 17 00:00:00 2001
From: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com>
Date: Thu, 8 Feb 2024 17:51:40 +0000
Subject: [PATCH 044/104] [IM] Improve index template tests (#176418)
Closes https://github.com/elastic/kibana/issues/176308
## Summary
This PR improves the `beforeEach` hook in the index template tests which
navigates to the Mappings step by using the data test subject of the
Mappings step instead of clicking the "Next" button until we reach this
step.
Flaky test runner build:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5081
---
.../apps/index_management/index_template_wizard.ts | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/x-pack/test/functional/apps/index_management/index_template_wizard.ts b/x-pack/test/functional/apps/index_management/index_template_wizard.ts
index 8776c6de06e40..2a16849f2f5de 100644
--- a/x-pack/test/functional/apps/index_management/index_template_wizard.ts
+++ b/x-pack/test/functional/apps/index_management/index_template_wizard.ts
@@ -117,13 +117,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.setValue('indexPatternsField', 'test-index-pattern');
// Go to Mappings step
- await pageObjects.indexManagement.clickNextButton();
- expect(await testSubjects.getVisibleText('stepTitle')).to.be(
- 'Component templates (optional)'
- );
- await pageObjects.indexManagement.clickNextButton();
- expect(await testSubjects.getVisibleText('stepTitle')).to.be('Index settings (optional)');
- await pageObjects.indexManagement.clickNextButton();
+ await testSubjects.click('formWizardStep-3');
expect(await testSubjects.getVisibleText('stepTitle')).to.be('Mappings (optional)');
});
From d404ea46e3476761f409c34628841272dfedee74 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Thu, 8 Feb 2024 10:58:55 -0700
Subject: [PATCH 045/104] [maps] fix map application is broken if you open a
map with layers or sources that do not exist (#176419)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes https://github.com/elastic/kibana/issues/176318
PR adds try/catch blocks around all usages of `createInstanceError`.
### Test steps
1) start kibana with `yarn start --run-examples`
2) create new map, add new layer `Weather data provided by NOAA`
3) save map
4) re-start kibana with `yarn start`
5) open map saved in step 3. Map should open and layer shoould display
error in legend
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/classes/layers/invalid_layer.ts | 89 +++++++++++++++++
.../maps/public/classes/layers/layer.tsx | 2 +-
.../maps/public/selectors/map_selectors.ts | 99 ++++++++++---------
.../apis/maps/maps_telemetry.ts | 14 +--
.../apps/maps/group4/layer_errors.js | 14 +++
.../fixtures/kbn_archiver/maps.json | 2 +-
6 files changed, 164 insertions(+), 56 deletions(-)
create mode 100644 x-pack/plugins/maps/public/classes/layers/invalid_layer.ts
diff --git a/x-pack/plugins/maps/public/classes/layers/invalid_layer.ts b/x-pack/plugins/maps/public/classes/layers/invalid_layer.ts
new file mode 100644
index 0000000000000..eed2ab37b32e0
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/layers/invalid_layer.ts
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable max-classes-per-file */
+
+import { i18n } from '@kbn/i18n';
+import { LayerDescriptor } from '../../../common/descriptor_types';
+import { AbstractLayer } from './layer';
+import { AbstractSource } from '../sources/source';
+import { IStyle } from '../styles/style';
+
+class InvalidSource extends AbstractSource {
+ constructor(id?: string) {
+ super({
+ id,
+ type: 'INVALID',
+ });
+ }
+}
+
+export class InvalidLayer extends AbstractLayer {
+ private readonly _error: Error;
+ private readonly _style: IStyle;
+
+ constructor(layerDescriptor: LayerDescriptor, error: Error) {
+ super({
+ layerDescriptor,
+ source: new InvalidSource(layerDescriptor.sourceDescriptor?.id),
+ });
+ this._error = error;
+ this._style = {
+ getType() {
+ return 'INVALID';
+ },
+ renderEditor() {
+ return null;
+ },
+ };
+ }
+
+ hasErrors() {
+ return true;
+ }
+
+ getErrors() {
+ return [
+ {
+ title: i18n.translate('xpack.maps.invalidLayer.errorTitle', {
+ defaultMessage: `Unable to create layer`,
+ }),
+ body: this._error.message,
+ },
+ ];
+ }
+
+ getStyleForEditing() {
+ return this._style;
+ }
+
+ getStyle() {
+ return this._style;
+ }
+
+ getCurrentStyle() {
+ return this._style;
+ }
+
+ getMbLayerIds() {
+ return [];
+ }
+
+ ownsMbLayerId() {
+ return false;
+ }
+
+ ownsMbSourceId() {
+ return false;
+ }
+
+ syncLayerWithMB() {}
+
+ getLayerTypeIconName() {
+ return 'error';
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index 1fccaf7f6d0a5..aa39cf017eb0d 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -245,7 +245,7 @@ export class AbstractLayer implements ILayer {
const sourceDisplayName = source
? await source.getDisplayName()
: await this.getSource().getDisplayName();
- return sourceDisplayName || `Layer ${this._descriptor.id}`;
+ return sourceDisplayName || this._descriptor.id;
}
async getAttributions(): Promise {
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts
index ec035ac1c5623..78950c1ab2e7f 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.ts
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts
@@ -23,6 +23,7 @@ import {
import { VectorStyle } from '../classes/styles/vector/vector_style';
import { isLayerGroup, LayerGroup } from '../classes/layers/layer_group';
import { HeatmapLayer } from '../classes/layers/heatmap_layer';
+import { InvalidLayer } from '../classes/layers/invalid_layer';
import { getTimeFilter } from '../kibana_services';
import { getChartsPaletteServiceGetColor } from '../reducers/non_serializable_instances';
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/copy_persistent_state';
@@ -76,54 +77,58 @@ export function createLayerInstance(
customIcons: CustomIcon[],
chartsPaletteServiceGetColor?: (value: string) => string | null
): ILayer {
- if (layerDescriptor.type === LAYER_TYPE.LAYER_GROUP) {
- return new LayerGroup({ layerDescriptor: layerDescriptor as LayerGroupDescriptor });
- }
+ try {
+ if (layerDescriptor.type === LAYER_TYPE.LAYER_GROUP) {
+ return new LayerGroup({ layerDescriptor: layerDescriptor as LayerGroupDescriptor });
+ }
- const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor);
- switch (layerDescriptor.type) {
- case LAYER_TYPE.RASTER_TILE:
- return new RasterTileLayer({ layerDescriptor, source: source as IRasterSource });
- case LAYER_TYPE.EMS_VECTOR_TILE:
- return new EmsVectorTileLayer({
- layerDescriptor: layerDescriptor as EMSVectorTileLayerDescriptor,
- source: source as EMSTMSSource,
- });
- case LAYER_TYPE.HEATMAP:
- return new HeatmapLayer({
- layerDescriptor: layerDescriptor as HeatmapLayerDescriptor,
- source: source as ESGeoGridSource,
- });
- case LAYER_TYPE.GEOJSON_VECTOR:
- return new GeoJsonVectorLayer({
- layerDescriptor: layerDescriptor as VectorLayerDescriptor,
- source: source as IVectorSource,
- joins: createJoinInstances(
- layerDescriptor as VectorLayerDescriptor,
- source as IVectorSource
- ),
- customIcons,
- chartsPaletteServiceGetColor,
- });
- case LAYER_TYPE.BLENDED_VECTOR:
- return new BlendedVectorLayer({
- layerDescriptor: layerDescriptor as VectorLayerDescriptor,
- source: source as IVectorSource,
- customIcons,
- chartsPaletteServiceGetColor,
- });
- case LAYER_TYPE.MVT_VECTOR:
- return new MvtVectorLayer({
- layerDescriptor: layerDescriptor as VectorLayerDescriptor,
- source: source as IVectorSource,
- joins: createJoinInstances(
- layerDescriptor as VectorLayerDescriptor,
- source as IVectorSource
- ),
- customIcons,
- });
- default:
- throw new Error(`Unrecognized layerType ${layerDescriptor.type}`);
+ const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor);
+ switch (layerDescriptor.type) {
+ case LAYER_TYPE.RASTER_TILE:
+ return new RasterTileLayer({ layerDescriptor, source: source as IRasterSource });
+ case LAYER_TYPE.EMS_VECTOR_TILE:
+ return new EmsVectorTileLayer({
+ layerDescriptor: layerDescriptor as EMSVectorTileLayerDescriptor,
+ source: source as EMSTMSSource,
+ });
+ case LAYER_TYPE.HEATMAP:
+ return new HeatmapLayer({
+ layerDescriptor: layerDescriptor as HeatmapLayerDescriptor,
+ source: source as ESGeoGridSource,
+ });
+ case LAYER_TYPE.GEOJSON_VECTOR:
+ return new GeoJsonVectorLayer({
+ layerDescriptor: layerDescriptor as VectorLayerDescriptor,
+ source: source as IVectorSource,
+ joins: createJoinInstances(
+ layerDescriptor as VectorLayerDescriptor,
+ source as IVectorSource
+ ),
+ customIcons,
+ chartsPaletteServiceGetColor,
+ });
+ case LAYER_TYPE.BLENDED_VECTOR:
+ return new BlendedVectorLayer({
+ layerDescriptor: layerDescriptor as VectorLayerDescriptor,
+ source: source as IVectorSource,
+ customIcons,
+ chartsPaletteServiceGetColor,
+ });
+ case LAYER_TYPE.MVT_VECTOR:
+ return new MvtVectorLayer({
+ layerDescriptor: layerDescriptor as VectorLayerDescriptor,
+ source: source as IVectorSource,
+ joins: createJoinInstances(
+ layerDescriptor as VectorLayerDescriptor,
+ source as IVectorSource
+ ),
+ customIcons,
+ });
+ default:
+ throw new Error(`Unrecognized layerType ${layerDescriptor.type}`);
+ }
+ } catch (error) {
+ return new InvalidLayer(layerDescriptor, error);
}
}
diff --git a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
index 92ae21c7c09c0..8f5c9ae95f8af 100644
--- a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
+++ b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
@@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) {
es_point_to_point: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 },
es_top_hits: { min: 1, max: 1, total: 2, avg: 0.07142857142857142 },
es_agg_heatmap: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 },
- esql: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 },
+ esql: { min: 1, max: 1, total: 2, avg: 0.07142857142857142 },
kbn_tms_raster: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 },
ems_basemap: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 },
ems_region: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 },
@@ -81,8 +81,8 @@ export default function ({ getService }: FtrProviderContext) {
min: 0,
},
dataSourcesCount: {
- avg: 1.1785714285714286,
- max: 6,
+ avg: 1.2142857142857142,
+ max: 7,
min: 1,
},
emsVectorLayersCount: {
@@ -104,8 +104,8 @@ export default function ({ getService }: FtrProviderContext) {
min: 1,
},
GEOJSON_VECTOR: {
- avg: 0.8214285714285714,
- max: 5,
+ avg: 0.8571428571428571,
+ max: 6,
min: 1,
},
HEATMAP: {
@@ -125,8 +125,8 @@ export default function ({ getService }: FtrProviderContext) {
},
},
layersCount: {
- avg: 1.2142857142857142,
- max: 7,
+ avg: 1.25,
+ max: 8,
min: 1,
},
},
diff --git a/x-pack/test/functional/apps/maps/group4/layer_errors.js b/x-pack/test/functional/apps/maps/group4/layer_errors.js
index e47c0e582c8f4..9f8a570a46d96 100644
--- a/x-pack/test/functional/apps/maps/group4/layer_errors.js
+++ b/x-pack/test/functional/apps/maps/group4/layer_errors.js
@@ -18,6 +18,20 @@ export default function ({ getPageObjects, getService }) {
await PageObjects.maps.loadSavedMap('layer with errors');
});
+ describe('Layer with invalid descriptor', () => {
+ const INVALID_LAYER_NAME = 'fff76ebb-57a6-4067-a373-1d191b9bd1a3';
+
+ it('should diplay error icon in legend', async () => {
+ await PageObjects.maps.hasErrorIconExistsOrFail(INVALID_LAYER_NAME);
+ });
+
+ it('should allow deletion of layer', async () => {
+ await PageObjects.maps.removeLayer(INVALID_LAYER_NAME);
+ const exists = await PageObjects.maps.doesLayerExist(INVALID_LAYER_NAME);
+ expect(exists).to.be(false);
+ });
+ });
+
describe('Layer with EsError', () => {
after(async () => {
await inspector.close();
diff --git a/x-pack/test/functional/fixtures/kbn_archiver/maps.json b/x-pack/test/functional/fixtures/kbn_archiver/maps.json
index 74f27e360cfa1..1ce5bc3fd6aa1 100644
--- a/x-pack/test/functional/fixtures/kbn_archiver/maps.json
+++ b/x-pack/test/functional/fixtures/kbn_archiver/maps.json
@@ -747,7 +747,7 @@
"version": "WzU1LDFd",
"attributes": {
"description": "",
- "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"KIBANA_TILEMAP\"},\"id\":\"ap0ys\",\"label\":\"Custom_TMS\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"RASTER_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"idThatDoesNotExitForEMSTile\",\"lightModeDefault\":\"road_map\"},\"temporary\":false,\"id\":\"plw9l\",\"label\":\"EMS_tiles\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"EMS_VECTOR_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"idThatDoesNotExitForEMSFileSource\"},\"temporary\":false,\"id\":\"2gro0\",\"label\":\"EMS_vector_shapes\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"f67fe707-95dd-46d6-89b8-82617b251b61\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"resolution\":\"COARSE\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_3_source_index_pattern\"},\"temporary\":false,\"id\":\"pl5qd\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32,\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"id\":\"a07072bb-3a92-4320-bd37-250ef6d04db7\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_4_source_index_pattern\"},\"temporary\":false,\"id\":\"9bw8h\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_5_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"origin\":\"join\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"type\":\"ES_TERM_SOURCE\",\"indexPatternRefName\":\"layer_5_join_0_index_pattern\"}}]},{\"sourceDescriptor\":{\"geoField\":\"destination\",\"scalingType\":\"LIMIT\",\"id\":\"ed01aac3-c0be-491e-98c9-f1cb6e37f185\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsGroupByTimeseries\":false,\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_6_source_index_pattern\"},\"id\":\"1cfaa7fa-dc73-419d-b362-7238e2270a1c\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]",
+ "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"KIBANA_TILEMAP\"},\"id\":\"ap0ys\",\"label\":\"Custom_TMS\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"RASTER_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"idThatDoesNotExitForEMSTile\",\"lightModeDefault\":\"road_map\"},\"temporary\":false,\"id\":\"plw9l\",\"label\":\"EMS_tiles\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"EMS_VECTOR_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"idThatDoesNotExitForEMSFileSource\"},\"temporary\":false,\"id\":\"2gro0\",\"label\":\"EMS_vector_shapes\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"f67fe707-95dd-46d6-89b8-82617b251b61\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"resolution\":\"COARSE\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_3_source_index_pattern\"},\"temporary\":false,\"id\":\"pl5qd\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32,\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"id\":\"a07072bb-3a92-4320-bd37-250ef6d04db7\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_4_source_index_pattern\"},\"temporary\":false,\"id\":\"9bw8h\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_5_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"origin\":\"join\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"type\":\"ES_TERM_SOURCE\",\"indexPatternRefName\":\"layer_5_join_0_index_pattern\"}}]},{\"sourceDescriptor\":{\"geoField\":\"destination\",\"scalingType\":\"LIMIT\",\"id\":\"ed01aac3-c0be-491e-98c9-f1cb6e37f185\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsGroupByTimeseries\":false,\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_6_source_index_pattern\"},\"id\":\"1cfaa7fa-dc73-419d-b362-7238e2270a1c\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false},{\"sourceDescriptor\":{\"id\":\"d4d6d4cf-58ee-4a0d-a792-532c0711fa2a\",\"type\":\"ESQL\"},\"id\":\"fff76ebb-57a6-4067-a373-1d191b9bd1a3\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#6092C0\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#4379aa\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]",
"mapStateJSON": "{\"adHocDataViews\":[],\"zoom\":3.38,\"center\":{\"lon\":76.34937,\"lat\":-77.25604},\"timeFilters\":{\"from\":\"now-7d\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"index\":\"561253e0-f731-11e8-8487-11b9dd924f96\",\"type\":\"custom\",\"disabled\":false,\"negate\":false,\"alias\":\"connections shard failure\",\"key\":\"query\",\"value\":\"{\\\"error_query\\\":{\\\"indices\\\":[{\\\"error_type\\\":\\\"exception\\\",\\\"message\\\":\\\"simulated shard failure\\\",\\\"name\\\":\\\"connections\\\"}]}}\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"error_query\":{\"indices\":[{\"error_type\":\"exception\",\"message\":\"simulated shard failure\",\"name\":\"connections\"}]}}}],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
"title": "layer with errors",
"uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"1cfaa7fa-dc73-419d-b362-7238e2270a1c\"]}"
From fa98e5871ce2f42954bfed0f32501e97fbacd069 Mon Sep 17 00:00:00 2001
From: "Quynh Nguyen (Quinn)" <43350163+qn895@users.noreply.github.com>
Date: Thu, 8 Feb 2024 12:49:04 -0600
Subject: [PATCH 046/104] [ML] Fix multi match query overriding filters in Data
visualizer and Data Drift (#176347)
This PR removes usage of `createMergedEsQuery` in favor of buildEsQuery.
It also fixes an intermittent issue with filters
https://github.com/elastic/kibana/issues/170472 not being honored when
query is partial/of multi match type.
It also improves adding/removing filter for empty values
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### Risk Matrix
Delete this section if it is not applicable to this PR.
Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.
When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:
| Risk | Probability | Severity | Mitigation/Notes |
|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/top_values/top_values.tsx | 16 +--
.../application/common/hooks/use_data.ts | 22 ++--
.../data_drift/use_data_drift_result.ts | 21 ++--
.../index_data_visualizer_view.tsx | 10 +-
.../components/search_panel/search_bar.tsx | 13 ++-
.../hooks/use_data_visualizer_grid_data.ts | 6 +-
.../utils/saved_search_utils.test.ts | 82 +-------------
.../utils/saved_search_utils.ts | 104 +++++-------------
8 files changed, 76 insertions(+), 198 deletions(-)
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
index 111b0a2113403..9ea517d45eea1 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
@@ -116,10 +116,10 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed,
>
{Array.isArray(topValues)
? topValues.map((value) => {
- const fieldValue =
- value.key_as_string ?? (value.key ? value.key.toString() : EMPTY_EXAMPLE);
+ const fieldValue = value.key_as_string ?? (value.key ? value.key.toString() : '');
+ const displayValue = fieldValue ?? EMPTY_EXAMPLE;
return (
-
+
= ({ stats, fieldFormat, barColor, compressed,
/>
{fieldName !== undefined &&
- fieldValue !== undefined &&
+ displayValue !== undefined &&
onAddFilter !== undefined ? (
= ({ stats, fieldFormat, barColor, compressed,
'xpack.dataVisualizer.dataGrid.field.addFilterAriaLabel',
{
defaultMessage: 'Filter for {fieldName}: "{value}"',
- values: { fieldName, value: fieldValue },
+ values: { fieldName, value: displayValue },
}
)}
- data-test-subj={`dvFieldDataTopValuesAddFilterButton-${fieldName}-${fieldValue}`}
+ data-test-subj={`dvFieldDataTopValuesAddFilterButton-${fieldName}-${displayValue}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
@@ -172,10 +172,10 @@ export const TopValues: FC
= ({ stats, fieldFormat, barColor, compressed,
'xpack.dataVisualizer.dataGrid.field.removeFilterAriaLabel',
{
defaultMessage: 'Filter out {fieldName}: "{value}"',
- values: { fieldName, value: fieldValue },
+ values: { fieldName, value: displayValue },
}
)}
- data-test-subj={`dvFieldDataTopValuesExcludeFilterButton-${fieldName}-${fieldValue}`}
+ data-test-subj={`dvFieldDataTopValuesExcludeFilterButton-${fieldName}-${displayValue}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
diff --git a/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts b/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts
index 65c882ba551d9..9b85720fc1df4 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts
@@ -14,9 +14,9 @@ import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { merge } from 'rxjs';
import { RandomSampler } from '@kbn/ml-random-sampler-utils';
import { mapAndFlattenFilters } from '@kbn/data-plugin/public';
-import { Query } from '@kbn/es-query';
+import { buildEsQuery, Query } from '@kbn/es-query';
import { SearchQueryLanguage } from '@kbn/ml-query-utils';
-import { createMergedEsQuery } from '../../index_data_visualizer/utils/saved_search_utils';
+import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { useDataDriftStateManagerContext } from '../../data_drift/use_state_manager';
import type { InitialSettings } from '../../data_drift/use_data_drift_result';
import {
@@ -74,7 +74,7 @@ export const useData = (
() => {
const searchQuery =
searchString !== undefined && searchQueryLanguage !== undefined
- ? { query: searchString, language: searchQueryLanguage }
+ ? ({ query: searchString, language: searchQueryLanguage } as Query)
: undefined;
const timefilterActiveBounds = timeRange ?? timefilter.getActiveBounds();
@@ -90,24 +90,24 @@ export const useData = (
runtimeFieldMap: selectedDataView.getRuntimeMappings(),
};
- const refQuery = createMergedEsQuery(
- searchQuery,
+ const refQuery = buildEsQuery(
+ selectedDataView,
+ searchQuery ?? [],
mapAndFlattenFilters([
...queryManager.filterManager.getFilters(),
...(referenceStateManager.filters ?? []),
]),
- selectedDataView,
- uiSettings
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
);
- const compQuery = createMergedEsQuery(
- searchQuery,
+ const compQuery = buildEsQuery(
+ selectedDataView,
+ searchQuery ?? [],
mapAndFlattenFilters([
...queryManager.filterManager.getFilters(),
...(comparisonStateManager.filters ?? []),
]),
- selectedDataView,
- uiSettings
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
);
return {
diff --git a/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts b/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts
index 07b74677e8ea9..05f24bdcb7b68 100644
--- a/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts
+++ b/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts
@@ -8,6 +8,7 @@
import { chunk, cloneDeep, flatten } from 'lodash';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { lastValueFrom } from 'rxjs';
+import { getEsQueryConfig } from '@kbn/data-plugin/common';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type {
@@ -30,7 +31,7 @@ import { computeChi2PValue, type Histogram } from '@kbn/ml-chi2test';
import { mapAndFlattenFilters } from '@kbn/data-plugin/public';
import type { AggregationsMultiTermsBucketKeys } from '@elastic/elasticsearch/lib/api/types';
-import { createMergedEsQuery } from '../index_data_visualizer/utils/saved_search_utils';
+import { buildEsQuery } from '@kbn/es-query';
import { useDataVisualizerKibana } from '../kibana_context';
import { useDataDriftStateManagerContext } from './use_state_manager';
@@ -758,18 +759,18 @@ export const useFetchDataComparisonResult = (
const kqlQuery =
searchString !== undefined && searchQueryLanguage !== undefined
- ? { query: searchString, language: searchQueryLanguage }
+ ? ({ query: searchString, language: searchQueryLanguage } as Query)
: undefined;
const refDataQuery = getDataComparisonQuery({
- searchQuery: createMergedEsQuery(
- kqlQuery,
+ searchQuery: buildEsQuery(
+ currentDataView,
+ kqlQuery ?? [],
mapAndFlattenFilters([
...queryManager.filterManager.getFilters(),
...(referenceStateManager.filters ?? []),
]),
- currentDataView,
- uiSettings
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
),
datetimeField: currentDataView?.timeFieldName,
runtimeFields,
@@ -827,14 +828,14 @@ export const useFetchDataComparisonResult = (
setLoaded(0.25);
const prodDataQuery = getDataComparisonQuery({
- searchQuery: createMergedEsQuery(
- kqlQuery,
+ searchQuery: buildEsQuery(
+ currentDataView,
+ kqlQuery ?? [],
mapAndFlattenFilters([
...queryManager.filterManager.getFilters(),
...(comparisonStateManager.filters ?? []),
]),
- currentDataView,
- uiSettings
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
),
datetimeField: currentDataView?.timeFieldName,
runtimeFields,
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
index cc17387886071..5d8ebe9e44d57 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
@@ -8,6 +8,7 @@
import { css } from '@emotion/react';
import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react';
import type { Required } from 'utility-types';
+import { getEsQueryConfig } from '@kbn/data-plugin/common';
import {
useEuiBreakpoint,
@@ -21,7 +22,7 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { type Filter, FilterStateStore, type Query } from '@kbn/es-query';
+import { type Filter, FilterStateStore, type Query, buildEsQuery } from '@kbn/es-query';
import { generateFilters } from '@kbn/data-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { usePageUrlState, useUrlState } from '@kbn/ml-url-state';
@@ -62,7 +63,6 @@ import { DocumentCountContent } from '../../../common/components/document_count_
import { OMIT_FIELDS } from '../../../../../common/constants';
import { SearchPanel } from '../search_panel';
import { ActionsPanel } from '../actions_panel';
-import { createMergedEsQuery } from '../../utils/saved_search_utils';
import { DataVisualizerDataViewManagement } from '../data_view_management';
import type { GetAdditionalLinks } from '../../../common/components/results_links';
import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
@@ -389,14 +389,14 @@ export const IndexDataVisualizerView: FC = (dataVi
language: searchQueryLanguage,
};
- const combinedQuery = createMergedEsQuery(
+ const combinedQuery = buildEsQuery(
+ currentDataView,
{
query: searchString || '',
language: searchQueryLanguage,
},
data.query.filterManager.getFilters() ?? [],
- currentDataView,
- uiSettings
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
);
setSearchParams({
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx
index 3ad691bbe11ce..d0f6812c4e253 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx
@@ -5,13 +5,13 @@
* 2.0.
*/
-import type { Filter, Query, TimeRange } from '@kbn/es-query';
+import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { isDefined } from '@kbn/ml-is-defined';
import { DataView } from '@kbn/data-views-plugin/common';
import type { SearchQueryLanguage } from '@kbn/ml-query-utils';
-import { createMergedEsQuery } from '../../utils/saved_search_utils';
+import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { useDataVisualizerKibana } from '../../../kibana_context';
export const SearchPanelContent = ({
@@ -63,16 +63,17 @@ export const SearchPanelContent = ({
const searchHandler = ({ query, filters }: { query?: Query; filters?: Filter[] }) => {
const mergedQuery = isDefined(query) ? query : searchInput;
const mergedFilters = isDefined(filters) ? filters : queryManager.filterManager.getFilters();
+
try {
if (mergedFilters) {
queryManager.filterManager.setFilters(mergedFilters);
}
- const combinedQuery = createMergedEsQuery(
- mergedQuery,
- queryManager.filterManager.getFilters() ?? [],
+ const combinedQuery = buildEsQuery(
dataView,
- uiSettings
+ mergedQuery ? [mergedQuery] : [],
+ queryManager.filterManager.getFilters() ?? [],
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
);
setSearchParams({
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
index b012d049ae04f..4570a2019af26 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
@@ -137,10 +137,11 @@ export const useDataVisualizerGridData = (
});
if (searchData === undefined || dataVisualizerListState.searchString !== '') {
- if (dataVisualizerListState.filters) {
+ if (filterManager) {
const globalFilters = filterManager?.getGlobalFilters();
- if (filterManager) filterManager.setFilters(dataVisualizerListState.filters);
+ if (dataVisualizerListState.filters)
+ filterManager.setFilters(dataVisualizerListState.filters);
if (globalFilters) filterManager?.addFilters(globalFilters);
}
return {
@@ -169,6 +170,7 @@ export const useDataVisualizerGridData = (
currentFilters,
}),
lastRefresh,
+ data.query.filterManager,
]);
const _timeBuckets = useTimeBuckets();
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts
index c43483a34e34c..2b25a5e8d2b8c 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts
@@ -5,14 +5,10 @@
* 2.0.
*/
-import {
- getQueryFromSavedSearchObject,
- createMergedEsQuery,
- getEsQueryFromSavedSearch,
-} from './saved_search_utils';
+import { getQueryFromSavedSearchObject, getEsQueryFromSavedSearch } from './saved_search_utils';
import type { SavedSearchSavedObject } from '../../../../common/types';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
-import { type Filter, FilterStateStore } from '@kbn/es-query';
+import { FilterStateStore } from '@kbn/es-query';
import { stubbedSavedObjectIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub';
import { DataView } from '@kbn/data-views-plugin/public';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
@@ -217,80 +213,6 @@ describe('getQueryFromSavedSearchObject()', () => {
});
});
-describe('createMergedEsQuery()', () => {
- const luceneQuery = {
- query: 'responsetime:>50',
- language: 'lucene',
- };
- const kqlQuery = {
- query: 'responsetime > 49',
- language: 'kuery',
- };
- const mockFilters: Filter[] = [
- {
- meta: {
- index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
- negate: false,
- disabled: false,
- alias: null,
- type: 'phrase',
- key: 'airline',
- params: {
- query: 'ASA',
- },
- },
- query: {
- match: {
- airline: {
- query: 'ASA',
- type: 'phrase',
- },
- },
- },
- $state: {
- store: 'appState' as FilterStateStore,
- },
- },
- ];
-
- it('return formatted ES bool query with both the original query and filters combined', () => {
- expect(createMergedEsQuery(luceneQuery, mockFilters)).toEqual({
- bool: {
- filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
- must: [{ query_string: { query: 'responsetime:>50' } }],
- must_not: [],
- should: [],
- },
- });
- expect(createMergedEsQuery(kqlQuery, mockFilters)).toEqual({
- bool: {
- filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
- minimum_should_match: 1,
- must_not: [],
- should: [{ range: { responsetime: { gt: '49' } } }],
- },
- });
- });
- it('return formatted ES bool query without filters ', () => {
- expect(createMergedEsQuery(luceneQuery)).toEqual({
- bool: {
- filter: [],
- must: [{ query_string: { query: 'responsetime:>50' } }],
- must_not: [],
- should: [],
- },
- });
- expect(createMergedEsQuery(kqlQuery)).toEqual({
- bool: {
- filter: [],
- minimum_should_match: 1,
- must_not: [],
- should: [{ range: { responsetime: { gt: '49' } } }],
- },
- });
- });
-});
-
describe('getEsQueryFromSavedSearch()', () => {
it('return undefined if saved search is not provided', () => {
expect(
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
index 04bc52bf08057..3ecc8a3a7a3d8 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
@@ -9,22 +9,15 @@
// `x-pack/plugins/apm/public/components/app/correlations/progress_controls.tsx`
import { cloneDeep } from 'lodash';
import { IUiSettingsClient } from '@kbn/core/public';
-import {
- fromKueryExpression,
- toElasticsearchQuery,
- buildQueryFromFilters,
- buildEsQuery,
- Query,
- Filter,
- AggregateQuery,
-} from '@kbn/es-query';
+import { buildEsQuery, Query, Filter } from '@kbn/es-query';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DataView } from '@kbn/data-views-plugin/public';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
-import { getEsQueryConfig, isQuery, SearchSource } from '@kbn/data-plugin/common';
+import { getEsQueryConfig, SearchSource } from '@kbn/data-plugin/common';
import { FilterManager, mapAndFlattenFilters } from '@kbn/data-plugin/public';
import { getDefaultDSLQuery } from '@kbn/ml-query-utils';
-import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '@kbn/ml-query-utils';
+import { SearchQueryLanguage } from '@kbn/ml-query-utils';
+import { isDefined } from '@kbn/ml-is-defined';
import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
/**
@@ -59,53 +52,8 @@ export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObjec
return parsed;
}
-/**
- * Create an Elasticsearch query that combines both lucene/kql query string and filters
- * Should also form a valid query if only the query or filters is provided
- */
-export function createMergedEsQuery(
- query?: Query | AggregateQuery | undefined,
- filters?: Filter[],
- dataView?: DataView,
- uiSettings?: IUiSettingsClient
-) {
- let combinedQuery = getDefaultDSLQuery() as QueryDslQueryContainer;
-
- if (isQuery(query) && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
- const ast = fromKueryExpression(query.query);
- if (query.query !== '') {
- combinedQuery = toElasticsearchQuery(ast, dataView);
- }
- if (combinedQuery.bool !== undefined) {
- const filterQuery = buildQueryFromFilters(filters, dataView);
-
- if (!Array.isArray(combinedQuery.bool.filter)) {
- combinedQuery.bool.filter =
- combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
- }
-
- if (!Array.isArray(combinedQuery.bool.must_not)) {
- combinedQuery.bool.must_not =
- combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
- }
-
- combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
- combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
- }
- } else {
- combinedQuery = buildEsQuery(
- dataView,
- query ? [query] : [],
- filters ? filters : [],
- uiSettings ? getEsQueryConfig(uiSettings) : undefined
- );
- }
-
- return combinedQuery;
-}
-
-function getSavedSearchSource(savedSearch: SavedSearch) {
- return savedSearch &&
+function getSavedSearchSource(savedSearch?: SavedSearch | null) {
+ return isDefined(savedSearch) &&
'searchSource' in savedSearch &&
savedSearch?.searchSource instanceof SearchSource
? savedSearch.searchSource
@@ -131,11 +79,15 @@ export function getEsQueryFromSavedSearch({
filters?: Filter[];
filterManager?: FilterManager;
}) {
- if (!dataView || !savedSearch) return;
+ if (!dataView && !savedSearch) return;
const userQuery = query;
const userFilters = filters;
+ if (filterManager && userFilters) {
+ filterManager.addFilters(userFilters);
+ }
+
const savedSearchSource = getSavedSearchSource(savedSearch);
// If saved search has a search source with nested parent
@@ -146,8 +98,8 @@ export function getEsQueryFromSavedSearch({
// Flattened query from search source may contain a clause that narrows the time range
// which might interfere with global time pickers so we need to remove
const savedQuery =
- cloneDeep(savedSearch.searchSource.getSearchRequestBody()?.query) ?? getDefaultDSLQuery();
- const timeField = savedSearch.searchSource.getField('index')?.timeFieldName;
+ cloneDeep(savedSearchSource.getSearchRequestBody()?.query) ?? getDefaultDSLQuery();
+ const timeField = savedSearchSource.getField('index')?.timeFieldName;
if (Array.isArray(savedQuery.bool.filter) && timeField !== undefined) {
savedQuery.bool.filter = savedQuery.bool.filter.filter(
@@ -155,6 +107,7 @@ export function getEsQueryFromSavedSearch({
!(c.hasOwnProperty('range') && c.range?.hasOwnProperty(timeField))
);
}
+
return {
searchQuery: savedQuery,
searchString: userQuery.query,
@@ -163,39 +116,38 @@ export function getEsQueryFromSavedSearch({
}
// If no saved search available, use user's query and filters
- if (!savedSearch && userQuery) {
- if (filterManager && userFilters) filterManager.addFilters(userFilters);
-
- const combinedQuery = createMergedEsQuery(
- userQuery,
- Array.isArray(userFilters) ? userFilters : [],
+ if (
+ !savedSearch &&
+ (userQuery || userFilters || (filterManager && filterManager.getGlobalFilters()?.length > 0))
+ ) {
+ const combinedQuery = buildEsQuery(
dataView,
- uiSettings
+ userQuery ?? [],
+ filterManager?.getFilters() ?? [],
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
);
return {
searchQuery: combinedQuery,
- searchString: userQuery.query,
- queryLanguage: userQuery.language as SearchQueryLanguage,
+ searchString: userQuery?.query ?? '',
+ queryLanguage: (userQuery?.language ?? 'kuery') as SearchQueryLanguage,
};
}
// If saved search available, merge saved search with the latest user query or filters
// which might differ from extracted saved search data
if (savedSearchSource) {
- const globalFilters = filterManager?.getGlobalFilters();
// FIXME: Add support for AggregateQuery type #150091
const currentQuery = userQuery ?? (savedSearchSource.getField('query') as Query);
const currentFilters =
userFilters ?? mapAndFlattenFilters(savedSearchSource.getField('filter') as Filter[]);
- if (filterManager) filterManager.setFilters(currentFilters);
- if (globalFilters) filterManager?.addFilters(globalFilters);
+ if (filterManager) filterManager.addFilters(currentFilters);
- const combinedQuery = createMergedEsQuery(
+ const combinedQuery = buildEsQuery(
+ dataView,
currentQuery,
filterManager ? filterManager?.getFilters() : currentFilters,
- dataView,
- uiSettings
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
);
return {
From ee34012cd9accce9f2b6bcb39cf4f342cff0f1ef Mon Sep 17 00:00:00 2001
From: Melissa Alvarez
Date: Thu, 8 Feb 2024 12:32:25 -0700
Subject: [PATCH 047/104] [ML] Anomaly Detection: Add single metric viewer
embeddable for dashboards (#175857)
## Summary
Related issue to [add ability to insert "Single Metric Viewer" into a
dashboard](https://github.com/elastic/kibana/issues/173555)
This PR adds the single metric viewer as an embeddable that can be added
to dashboards.
### NOTE FOR TESTING:
This PR relies on the SMV fix for 'metric' jobs
https://github.com/elastic/kibana/pull/176354
If that fix has not been merged, you will need to find
`getAnomalyRecordsSchema` definition and add `functionDescription:
schema.maybe(schema.nullable(schema.string())),` to it for local
testing.
### Screenshots of feature
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../ml/anomaly_utils/anomaly_utils.ts | 4 +
.../services/field_format_service.ts | 80 +-
.../services/field_format_service_factory.ts | 17 +
.../services/forecast_service.d.ts | 2 +
.../services/forecast_service_provider.ts | 395 ++++++++
.../services/results_service/index.ts | 13 +
.../plot_function_controls.tsx | 12 +-
.../series_controls/series_controls.tsx | 38 +-
.../timeseries_chart/timeseries_chart.js | 62 +-
.../timeseries_chart_with_tooltip.tsx | 13 +-
.../get_controls_for_detector.ts | 7 +-
.../get_function_description.ts | 5 +-
.../timeseriesexplorer/timeseriesexplorer.js | 45 +-
.../timeseriesexplorer_constants.ts | 4 +-
.../index.ts | 8 +
.../timeseriesexplorer_checkbox.tsx | 25 +
.../timeseriesexplorer_embeddable_chart.js | 897 ++++++++++++++++++
.../timeseriesexplorer_help_popover.tsx | 20 +-
.../get_focus_data.ts | 14 +-
.../get_timeseriesexplorer_default_state.ts | 46 +
.../timeseriesexplorer_utils/index.ts | 1 +
.../time_series_search_service.ts | 187 ++++
.../public/application/util/index_service.ts | 55 ++
.../public/application/util/time_buckets.d.ts | 1 +
.../application/util/time_buckets_service.ts | 57 ++
.../util/time_series_explorer_service.ts | 648 +++++++++++++
.../common/resolve_job_selection.tsx | 5 +-
.../ml/public/embeddables/constants.ts | 1 +
x-pack/plugins/ml/public/embeddables/index.ts | 5 +-
.../single_metric_viewer/_index.scss | 6 +
...eddable_single_metric_viewer_container.tsx | 204 ++++
...le_single_metric_viewer_container_lazy.tsx | 12 +
.../embeddables/single_metric_viewer/index.ts | 8 +
.../single_metric_viewer_embeddable.tsx | 136 +++
...single_metric_viewer_embeddable_factory.ts | 131 +++
.../single_metric_viewer_initializer.tsx | 157 +++
.../single_metric_viewer_setup_flyout.tsx | 75 ++
...use_single_metric_viewer_input_resolver.ts | 46 +
x-pack/plugins/ml/public/embeddables/types.ts | 35 +
39 files changed, 3341 insertions(+), 136 deletions(-)
create mode 100644 x-pack/plugins/ml/public/application/services/field_format_service_factory.ts
create mode 100644 x-pack/plugins/ml/public/application/services/forecast_service_provider.ts
create mode 100644 x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/index.ts
create mode 100644 x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_checkbox.tsx
create mode 100644 x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js
create mode 100644 x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_timeseriesexplorer_default_state.ts
create mode 100644 x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service.ts
create mode 100644 x-pack/plugins/ml/public/application/util/index_service.ts
create mode 100644 x-pack/plugins/ml/public/application/util/time_buckets_service.ts
create mode 100644 x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/_index.scss
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container_lazy.tsx
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/index.ts
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx
create mode 100644 x-pack/plugins/ml/public/embeddables/single_metric_viewer/use_single_metric_viewer_input_resolver.ts
diff --git a/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts b/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts
index 4865aed1e4e97..8ef9fee14a273 100644
--- a/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts
+++ b/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts
@@ -59,6 +59,10 @@ export interface MlEntityField {
* Optional entity field operation
*/
operation?: MlEntityFieldOperation;
+ /**
+ * Optional cardinality of field
+ */
+ cardinality?: number;
}
// List of function descriptions for which actual values from record level results should be displayed.
diff --git a/x-pack/plugins/ml/public/application/services/field_format_service.ts b/x-pack/plugins/ml/public/application/services/field_format_service.ts
index a43e134f84cfb..8519a13e7d7bc 100644
--- a/x-pack/plugins/ml/public/application/services/field_format_service.ts
+++ b/x-pack/plugins/ml/public/application/services/field_format_service.ts
@@ -8,16 +8,20 @@
import { mlFunctionToESAggregation } from '../../../common/util/job_utils';
import { getDataViewById, getDataViewIdFromName } from '../util/index_utils';
import { mlJobService } from './job_service';
+import type { MlIndexUtils } from '../util/index_service';
+import type { MlApiServices } from './ml_api_service';
type FormatsByJobId = Record;
type IndexPatternIdsByJob = Record;
// Service for accessing FieldFormat objects configured for a Kibana data view
// for use in formatting the actual and typical values from anomalies.
-class FieldFormatService {
+export class FieldFormatService {
indexPatternIdsByJob: IndexPatternIdsByJob = {};
formatsByJob: FormatsByJobId = {};
+ constructor(private mlApiServices?: MlApiServices, private mlIndexUtils?: MlIndexUtils) {}
+
// Populate the service with the FieldFormats for the list of jobs with the
// specified IDs. List of Kibana data views is passed, with a title
// attribute set in each pattern which will be compared to the indices
@@ -32,10 +36,17 @@ class FieldFormatService {
(
await Promise.all(
jobIds.map(async (jobId) => {
- const jobObj = mlJobService.getJob(jobId);
+ const getDataViewId = this.mlIndexUtils?.getDataViewIdFromName ?? getDataViewIdFromName;
+ let jobObj;
+ if (this.mlApiServices) {
+ const { jobs } = await this.mlApiServices.getJobs({ jobId });
+ jobObj = jobs[0];
+ } else {
+ jobObj = mlJobService.getJob(jobId);
+ }
return {
jobId,
- dataViewId: await getDataViewIdFromName(jobObj.datafeed_config.indices.join(',')),
+ dataViewId: await getDataViewId(jobObj.datafeed_config!.indices.join(',')),
};
})
)
@@ -68,41 +79,40 @@ class FieldFormatService {
}
}
- getFormatsForJob(jobId: string): Promise {
- return new Promise((resolve, reject) => {
- const jobObj = mlJobService.getJob(jobId);
- const detectors = jobObj.analysis_config.detectors || [];
- const formatsByDetector: any[] = [];
+ async getFormatsForJob(jobId: string): Promise {
+ let jobObj;
+ const getDataView = this.mlIndexUtils?.getDataViewById ?? getDataViewById;
+ if (this.mlApiServices) {
+ const { jobs } = await this.mlApiServices.getJobs({ jobId });
+ jobObj = jobs[0];
+ } else {
+ jobObj = mlJobService.getJob(jobId);
+ }
+ const detectors = jobObj.analysis_config.detectors || [];
+ const formatsByDetector: any[] = [];
- const dataViewId = this.indexPatternIdsByJob[jobId];
- if (dataViewId !== undefined) {
- // Load the full data view configuration to obtain the formats of each field.
- getDataViewById(dataViewId)
- .then((dataView) => {
- // Store the FieldFormat for each job by detector_index.
- const fieldList = dataView.fields;
- detectors.forEach((dtr) => {
- const esAgg = mlFunctionToESAggregation(dtr.function);
- // distinct_count detectors should fall back to the default
- // formatter as the values are just counts.
- if (dtr.field_name !== undefined && esAgg !== 'cardinality') {
- const field = fieldList.getByName(dtr.field_name);
- if (field !== undefined) {
- formatsByDetector[dtr.detector_index!] = dataView.getFormatterForField(field);
- }
- }
- });
+ const dataViewId = this.indexPatternIdsByJob[jobId];
+ if (dataViewId !== undefined) {
+ // Load the full data view configuration to obtain the formats of each field.
+ const dataView = await getDataView(dataViewId);
+ // Store the FieldFormat for each job by detector_index.
+ const fieldList = dataView.fields;
+ detectors.forEach((dtr) => {
+ const esAgg = mlFunctionToESAggregation(dtr.function);
+ // distinct_count detectors should fall back to the default
+ // formatter as the values are just counts.
+ if (dtr.field_name !== undefined && esAgg !== 'cardinality') {
+ const field = fieldList.getByName(dtr.field_name);
+ if (field !== undefined) {
+ formatsByDetector[dtr.detector_index!] = dataView.getFormatterForField(field);
+ }
+ }
+ });
+ }
- resolve(formatsByDetector);
- })
- .catch((err) => {
- reject(err);
- });
- } else {
- resolve(formatsByDetector);
- }
- });
+ return formatsByDetector;
}
}
export const mlFieldFormatService = new FieldFormatService();
+export type MlFieldFormatService = typeof mlFieldFormatService;
diff --git a/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts b/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts
new file mode 100644
index 0000000000000..daefab69154c5
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { type MlFieldFormatService, FieldFormatService } from './field_format_service';
+import type { MlIndexUtils } from '../util/index_service';
+import type { MlApiServices } from './ml_api_service';
+
+export function fieldFormatServiceFactory(
+ mlApiServices: MlApiServices,
+ mlIndexUtils: MlIndexUtils
+): MlFieldFormatService {
+ return new FieldFormatService(mlApiServices, mlIndexUtils);
+}
diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
index 0bfd8f56385d6..55df37b2307da 100644
--- a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
@@ -32,3 +32,5 @@ export const mlForecastService: {
getForecastDateRange: (job: Job, forecastId: string) => Promise;
};
+
+export type MlForecastService = typeof mlForecastService;
diff --git a/x-pack/plugins/ml/public/application/services/forecast_service_provider.ts b/x-pack/plugins/ml/public/application/services/forecast_service_provider.ts
new file mode 100644
index 0000000000000..c776a79a6f475
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/services/forecast_service_provider.ts
@@ -0,0 +1,395 @@
+/*
+ * 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.
+ */
+
+// Service for carrying out requests to run ML forecasts and to obtain
+// data on forecasts that have been performed.
+import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { get, find, each } from 'lodash';
+import { map } from 'rxjs/operators';
+import type { MlApiServices } from './ml_api_service';
+import type { Job } from '../../../common/types/anomaly_detection_jobs';
+
+export interface AggType {
+ avg: string;
+ max: string;
+ min: string;
+}
+
+// TODO Consolidate with legacy code in
+// `x-pack/plugins/ml/public/application/services/forecast_service.js` and
+// `x-pack/plugins/ml/public/application/services/forecast_service.d.ts`.
+export function forecastServiceProvider(mlApiServices: MlApiServices) {
+ return {
+ // Gets a basic summary of the most recently run forecasts for the specified
+ // job, with results at or later than the supplied timestamp.
+ // Extra query object can be supplied, or pass null if no additional query.
+ // Returned response contains a forecasts property, which is an array of objects
+ // containing id, earliest and latest keys.
+ getForecastsSummary(job: Job, query: any, earliestMs: number, maxResults: any) {
+ return new Promise((resolve, reject) => {
+ const obj: { success: boolean; forecasts: Record } = {
+ success: true,
+ forecasts: [],
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the job ID, result type and earliest time, plus
+ // the additional query if supplied.
+ const filterCriteria = [
+ {
+ term: { result_type: 'model_forecast_request_stats' },
+ },
+ {
+ term: { job_id: job.job_id },
+ },
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ ];
+
+ if (query) {
+ filterCriteria.push(query);
+ }
+
+ mlApiServices.results
+ .anomalySearch(
+ {
+ // @ts-expect-error SearchRequest type has not been updated to include size
+ size: maxResults,
+ body: {
+ query: {
+ bool: {
+ filter: filterCriteria,
+ },
+ },
+ sort: [{ forecast_create_timestamp: { order: 'desc' } }],
+ },
+ },
+ [job.job_id]
+ )
+ .then((resp) => {
+ if (resp.hits.total.value > 0) {
+ obj.forecasts = resp.hits.hits.map((hit) => hit._source);
+ }
+
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
+ });
+ },
+ // Obtains the earliest and latest timestamps for the forecast data from
+ // the forecast with the specified ID.
+ // Returned response contains earliest and latest properties which are the
+ // timestamps of the first and last model_forecast results.
+ getForecastDateRange(job: Job, forecastId: string) {
+ return new Promise((resolve, reject) => {
+ const obj = {
+ success: true,
+ earliest: null,
+ latest: null,
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the job ID, forecast ID, result type and time range.
+ const filterCriteria = [
+ {
+ query_string: {
+ query: 'result_type:model_forecast',
+ analyze_wildcard: true,
+ },
+ },
+ {
+ term: { job_id: job.job_id },
+ },
+ {
+ term: { forecast_id: forecastId },
+ },
+ ];
+
+ // TODO - add in criteria for detector index and entity fields (by, over, partition)
+ // once forecasting with these parameters is supported.
+
+ mlApiServices.results
+ .anomalySearch(
+ {
+ // @ts-expect-error SearchRequest type has not been updated to include size
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: filterCriteria,
+ },
+ },
+ aggs: {
+ earliest: {
+ min: {
+ field: 'timestamp',
+ },
+ },
+ latest: {
+ max: {
+ field: 'timestamp',
+ },
+ },
+ },
+ },
+ },
+ [job.job_id]
+ )
+ .then((resp) => {
+ obj.earliest = get(resp, 'aggregations.earliest.value', null);
+ obj.latest = get(resp, 'aggregations.latest.value', null);
+ if (obj.earliest === null || obj.latest === null) {
+ reject(resp);
+ } else {
+ resolve(obj);
+ }
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
+ });
+ },
+ // Obtains the requested forecast model data for the forecast with the specified ID.
+ getForecastData(
+ job: Job,
+ detectorIndex: number,
+ forecastId: string,
+ entityFields: any,
+ earliestMs: number,
+ latestMs: number,
+ intervalMs: number,
+ aggType?: AggType
+ ) {
+ // Extract the partition, by, over fields on which to filter.
+ const criteriaFields = [];
+ const detector = job.analysis_config.detectors[detectorIndex];
+ if (detector.partition_field_name !== undefined) {
+ const partitionEntity = find(entityFields, { fieldName: detector.partition_field_name });
+ if (partitionEntity !== undefined) {
+ criteriaFields.push(
+ { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName },
+ { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }
+ );
+ }
+ }
+
+ if (detector.over_field_name !== undefined) {
+ const overEntity = find(entityFields, { fieldName: detector.over_field_name });
+ if (overEntity !== undefined) {
+ criteriaFields.push(
+ { fieldName: 'over_field_name', fieldValue: overEntity.fieldName },
+ { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }
+ );
+ }
+ }
+
+ if (detector.by_field_name !== undefined) {
+ const byEntity = find(entityFields, { fieldName: detector.by_field_name });
+ if (byEntity !== undefined) {
+ criteriaFields.push(
+ { fieldName: 'by_field_name', fieldValue: byEntity.fieldName },
+ { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }
+ );
+ }
+ }
+
+ const obj: { success: boolean; results: Record } = {
+ success: true,
+ results: {},
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the job ID, forecast ID, detector index, result type and time range.
+ const filterCriteria: estypes.QueryDslQueryContainer[] = [
+ {
+ query_string: {
+ query: 'result_type:model_forecast',
+ analyze_wildcard: true,
+ },
+ },
+ {
+ term: { job_id: job.job_id },
+ },
+ {
+ term: { forecast_id: forecastId },
+ },
+ {
+ term: { detector_index: detectorIndex },
+ },
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ ];
+
+ // Add in term queries for each of the specified criteria.
+ each(criteriaFields, (criteria) => {
+ filterCriteria.push({
+ term: {
+ [criteria.fieldName]: criteria.fieldValue,
+ },
+ });
+ });
+
+ // If an aggType object has been passed in, use it.
+ // Otherwise default to avg, min and max aggs for the
+ // forecast prediction, upper and lower
+ const forecastAggs =
+ aggType === undefined
+ ? { avg: 'avg', max: 'max', min: 'min' }
+ : {
+ avg: aggType.avg,
+ max: aggType.max,
+ min: aggType.min,
+ };
+
+ return mlApiServices.results
+ .anomalySearch$(
+ {
+ // @ts-expect-error SearchRequest type has not been updated to include size
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: filterCriteria,
+ },
+ },
+ aggs: {
+ times: {
+ date_histogram: {
+ field: 'timestamp',
+ fixed_interval: `${intervalMs}ms`,
+ min_doc_count: 1,
+ },
+ aggs: {
+ prediction: {
+ [forecastAggs.avg]: {
+ field: 'forecast_prediction',
+ },
+ },
+ forecastUpper: {
+ [forecastAggs.max]: {
+ field: 'forecast_upper',
+ },
+ },
+ forecastLower: {
+ [forecastAggs.min]: {
+ field: 'forecast_lower',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ [job.job_id]
+ )
+ .pipe(
+ map((resp) => {
+ const aggregationsByTime = get(resp, ['aggregations', 'times', 'buckets'], []);
+ each(aggregationsByTime, (dataForTime) => {
+ const time = dataForTime.key;
+ obj.results[time] = {
+ prediction: get(dataForTime, ['prediction', 'value']),
+ forecastUpper: get(dataForTime, ['forecastUpper', 'value']),
+ forecastLower: get(dataForTime, ['forecastLower', 'value']),
+ };
+ });
+
+ return obj;
+ })
+ );
+ },
+ // Runs a forecast
+ runForecast(jobId: string, duration?: string) {
+ // eslint-disable-next-line no-console
+ console.log('ML forecast service run forecast with duration:', duration);
+ return new Promise((resolve, reject) => {
+ mlApiServices
+ .forecast({
+ jobId,
+ duration,
+ })
+ .then((resp) => {
+ resolve(resp);
+ })
+ .catch((err) => {
+ reject(err);
+ });
+ });
+ },
+ // Gets stats for a forecast that has been run on the specified job.
+ // Returned response contains a stats property, including
+ // forecast_progress (a value from 0 to 1),
+ // and forecast_status ('finished' when complete) properties.
+ getForecastRequestStats(job: Job, forecastId: string) {
+ return new Promise((resolve, reject) => {
+ const obj = {
+ success: true,
+ stats: {},
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the job ID, result type and earliest time.
+ const filterCriteria = [
+ {
+ query_string: {
+ query: 'result_type:model_forecast_request_stats',
+ analyze_wildcard: true,
+ },
+ },
+ {
+ term: { job_id: job.job_id },
+ },
+ {
+ term: { forecast_id: forecastId },
+ },
+ ];
+
+ mlApiServices.results
+ .anomalySearch(
+ {
+ // @ts-expect-error SearchRequest type has not been updated to include size
+ size: 1,
+ body: {
+ query: {
+ bool: {
+ filter: filterCriteria,
+ },
+ },
+ },
+ },
+ [job.job_id]
+ )
+ .then((resp) => {
+ if (resp.hits.total.value > 0) {
+ obj.stats = resp.hits.hits[0]._source;
+ }
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
+ });
+ },
+ };
+}
+
+export type MlForecastService = ReturnType;
diff --git a/x-pack/plugins/ml/public/application/services/results_service/index.ts b/x-pack/plugins/ml/public/application/services/results_service/index.ts
index 4fe6b7add2a6b..883b54dd73e72 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/index.ts
@@ -5,9 +5,11 @@
* 2.0.
*/
+import { useMemo } from 'react';
import { resultsServiceRxProvider } from './result_service_rx';
import { resultsServiceProvider } from './results_service';
import { ml, MlApiServices } from '../ml_api_service';
+import { useMlKibana } from '../../contexts/kibana';
export type MlResultsService = typeof mlResultsService;
@@ -29,3 +31,14 @@ export function mlResultsServiceProvider(mlApiServices: MlApiServices) {
...resultsServiceRxProvider(mlApiServices),
};
}
+
+export function useMlResultsService(): MlResultsService {
+ const {
+ services: {
+ mlServices: { mlApiServices },
+ },
+ } = useMlKibana();
+
+ const resultsService = useMemo(() => mlResultsServiceProvider(mlApiServices), [mlApiServices]);
+ return resultsService;
+}
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx
index c707bbee2c5b9..493e74755588b 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx
@@ -9,9 +9,11 @@ import React, { useCallback, useEffect } from 'react';
import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
+import { MlJob } from '@elastic/elasticsearch/lib/api/types';
import { mlJobService } from '../../../services/job_service';
import { getFunctionDescription, isMetricDetector } from '../../get_function_description';
import { useToastNotificationService } from '../../../services/toast_notification_service';
+import { useMlResultsService } from '../../../services/results_service';
import type { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs';
const plotByFunctionOptions = [
@@ -36,6 +38,7 @@ const plotByFunctionOptions = [
];
export const PlotByFunctionControls = ({
functionDescription,
+ job,
setFunctionDescription,
selectedDetectorIndex,
selectedJobId,
@@ -43,6 +46,7 @@ export const PlotByFunctionControls = ({
entityControlsCount,
}: {
functionDescription: undefined | string;
+ job?: CombinedJob | MlJob;
setFunctionDescription: (func: string) => void;
selectedDetectorIndex: number;
selectedJobId: string;
@@ -50,6 +54,7 @@ export const PlotByFunctionControls = ({
entityControlsCount: number;
}) => {
const toastNotificationService = useToastNotificationService();
+ const mlResultsService = useMlResultsService();
const getFunctionDescriptionToPlot = useCallback(
async (
@@ -65,18 +70,19 @@ export const PlotByFunctionControls = ({
selectedJobId: _selectedJobId,
selectedJob: _selectedJob,
},
- toastNotificationService
+ toastNotificationService,
+ mlResultsService
);
setFunctionDescription(functionToPlot);
},
- [setFunctionDescription, toastNotificationService]
+ [setFunctionDescription, toastNotificationService, mlResultsService]
);
useEffect(() => {
if (functionDescription !== undefined) {
return;
}
- const selectedJob = mlJobService.getJob(selectedJobId);
+ const selectedJob = (job ?? mlJobService.getJob(selectedJobId)) as CombinedJob;
// if no controls, it's okay to fetch
// if there are series controls, only fetch if user has selected something
const validEntities =
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx
index 23bc2f80eb1a8..666d56f15fbc8 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx
@@ -12,9 +12,10 @@ import { debounce } from 'lodash';
import { lastValueFrom } from 'rxjs';
import { useStorage } from '@kbn/ml-local-storage';
import type { MlEntityFieldType } from '@kbn/ml-anomaly-utils';
+import { MlJob } from '@elastic/elasticsearch/lib/api/types';
import { EntityControl } from '../entity_control';
import { mlJobService } from '../../../services/job_service';
-import { Detector, JobId } from '../../../../../common/types/anomaly_detection_jobs';
+import { CombinedJob, Detector, JobId } from '../../../../../common/types/anomaly_detection_jobs';
import { useMlKibana } from '../../../contexts/kibana';
import { APP_STATE_ACTION } from '../../timeseriesexplorer_constants';
import {
@@ -67,12 +68,13 @@ const getDefaultFieldConfig = (
};
interface SeriesControlsProps {
- selectedDetectorIndex: number;
- selectedJobId: JobId;
- bounds: any;
appStateHandler: Function;
+ bounds: any;
+ functionDescription?: string;
+ job?: CombinedJob | MlJob;
+ selectedDetectorIndex: number;
selectedEntities: Record;
- functionDescription: string;
+ selectedJobId: JobId;
setFunctionDescription: (func: string) => void;
}
@@ -80,13 +82,14 @@ interface SeriesControlsProps {
* Component for handling the detector and entities controls.
*/
export const SeriesControls: FC = ({
- bounds,
- selectedDetectorIndex,
- selectedJobId,
appStateHandler,
+ bounds,
children,
- selectedEntities,
functionDescription,
+ job,
+ selectedDetectorIndex,
+ selectedEntities,
+ selectedJobId,
setFunctionDescription,
}) => {
const {
@@ -97,7 +100,11 @@ export const SeriesControls: FC = ({
},
} = useMlKibana();
- const selectedJob = useMemo(() => mlJobService.getJob(selectedJobId), [selectedJobId]);
+ const selectedJob: CombinedJob | MlJob = useMemo(
+ () => job ?? mlJobService.getJob(selectedJobId),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [selectedJobId]
+ );
const isModelPlotEnabled = !!selectedJob.model_plot_config?.enabled;
@@ -108,11 +115,17 @@ export const SeriesControls: FC = ({
index: number;
detector_description: Detector['detector_description'];
}> = useMemo(() => {
- return getViewableDetectors(selectedJob);
+ return getViewableDetectors(selectedJob as CombinedJob);
}, [selectedJob]);
const entityControls = useMemo(() => {
- return getControlsForDetector(selectedDetectorIndex, selectedEntities, selectedJobId);
+ return getControlsForDetector(
+ selectedDetectorIndex,
+ selectedEntities,
+ selectedJobId,
+ selectedJob as CombinedJob
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDetectorIndex, selectedEntities, selectedJobId]);
const [storageFieldsConfig, setStorageFieldsConfig] = useStorage<
@@ -318,6 +331,7 @@ export const SeriesControls: FC = ({
);
})}
`anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`);
// Add rectangular markers for any scheduled events.
- const scheduledEventMarkers = d3
+ const scheduledEventMarkers = chartElement
.select('.focus-chart-markers')
.selectAll('.scheduled-event-marker')
.data(data.filter((d) => d.scheduledEvents !== undefined));
@@ -898,7 +915,7 @@ class TimeseriesChartIntl extends Component {
.attr('d', this.focusValuesLine(focusForecastData))
.classed('hidden', !showForecast);
- const forecastDots = d3
+ const forecastDots = chartElement
.select('.focus-chart-markers.forecast')
.selectAll('.metric-value')
.data(focusForecastData);
@@ -1007,7 +1024,7 @@ class TimeseriesChartIntl extends Component {
const chartElement = d3.select(this.rootNode);
chartElement.selectAll('.focus-zoom a').on('click', function () {
d3.event.preventDefault();
- setZoomInterval(d3.select(this).attr('data-ms'));
+ setZoomInterval(this.getAttribute('data-ms'));
});
}
@@ -1129,7 +1146,7 @@ class TimeseriesChartIntl extends Component {
.attr('y2', brushChartHeight);
// Add x axis.
- const timeBuckets = getTimeBucketsFromCache();
+ const timeBuckets = this.getTimeBuckets();
timeBuckets.setInterval('auto');
timeBuckets.setBounds(bounds);
const xAxisTickFormat = timeBuckets.getScaledDateFormat();
@@ -1328,6 +1345,7 @@ class TimeseriesChartIntl extends Component {
`);
+ const that = this;
function brushing() {
const brushExtent = brush.extent();
mask.reveal(brushExtent);
@@ -1345,11 +1363,11 @@ class TimeseriesChartIntl extends Component {
topBorder.attr('width', topBorderWidth);
const isEmpty = brush.empty();
- d3.selectAll('.brush-handle').style('visibility', isEmpty ? 'hidden' : 'visible');
+ const chartElement = d3.select(that.rootNode);
+ chartElement.selectAll('.brush-handle').style('visibility', isEmpty ? 'hidden' : 'visible');
}
brushing();
- const that = this;
function brushed() {
const isEmpty = brush.empty();
const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent();
@@ -1478,18 +1496,19 @@ class TimeseriesChartIntl extends Component {
// Sets the extent of the brush on the context chart to the
// supplied from and to Date objects.
setContextBrushExtent = (from, to) => {
+ const chartElement = d3.select(this.rootNode);
const brush = this.brush;
const brushExtent = brush.extent();
const newExtent = [from, to];
brush.extent(newExtent);
- brush(d3.select('.brush'));
+ brush(chartElement.select('.brush'));
if (
newExtent[0].getTime() !== brushExtent[0].getTime() ||
newExtent[1].getTime() !== brushExtent[1].getTime()
) {
- brush.event(d3.select('.brush'));
+ brush.event(chartElement.select('.brush'));
}
};
@@ -1867,12 +1886,13 @@ class TimeseriesChartIntl extends Component {
anomalyTime,
focusAggregationInterval
);
+ const chartElement = d3.select(this.rootNode);
// Render an additional highlighted anomaly marker on the focus chart.
// TODO - plot anomaly markers for cases where there is an anomaly due
// to the absence of data and model plot is enabled.
if (markerToSelect !== undefined) {
- const selectedMarker = d3
+ const selectedMarker = chartElement
.select('.focus-chart-markers')
.selectAll('.focus-chart-highlighted-marker')
.data([markerToSelect]);
@@ -1905,7 +1925,6 @@ class TimeseriesChartIntl extends Component {
// Display the chart tooltip for this marker.
// Note the values of the record and marker may differ depending on the levels of aggregation.
- const chartElement = d3.select(this.rootNode);
const anomalyMarker = chartElement.selectAll(
'.focus-chart-markers .anomaly-marker.highlighted'
);
@@ -1916,7 +1935,8 @@ class TimeseriesChartIntl extends Component {
}
unhighlightFocusChartAnomaly() {
- d3.select('.focus-chart-markers').selectAll('.anomaly-marker.highlighted').remove();
+ const chartElement = d3.select(this.rootNode);
+ chartElement.select('.focus-chart-markers').selectAll('.anomaly-marker.highlighted').remove();
this.props.tooltipService.hide();
}
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx
index 66da1e4222887..b9e09158bf280 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx
@@ -15,7 +15,7 @@ import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'
import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search';
import { Annotation } from '../../../../../common/types/annotations';
import { useMlKibana, useNotifications } from '../../../contexts/kibana';
-import { getBoundsRoundedToInterval } from '../../../util/time_buckets';
+import { useTimeBucketsService } from '../../../util/time_buckets_service';
import { getControlsForDetector } from '../../get_controls_for_detector';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
import { SourceIndicesWithGeoFields } from '../../../explorer/explorer_utils';
@@ -23,6 +23,7 @@ import { SourceIndicesWithGeoFields } from '../../../explorer/explorer_utils';
interface TimeSeriesChartWithTooltipsProps {
bounds: any;
detectorIndex: number;
+ embeddableMode?: boolean;
renderFocusChartOnly: boolean;
selectedJob: CombinedJob;
selectedEntities: Record;
@@ -41,6 +42,7 @@ interface TimeSeriesChartWithTooltipsProps {
export const TimeSeriesChartWithTooltips: FC = ({
bounds,
detectorIndex,
+ embeddableMode,
renderFocusChartOnly,
selectedJob,
selectedEntities,
@@ -80,13 +82,19 @@ export const TimeSeriesChartWithTooltips: FC =
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ const mlTimeBucketsService = useTimeBucketsService();
+
useEffect(() => {
let unmounted = false;
const entities = getControlsForDetector(detectorIndex, selectedEntities, selectedJob.job_id);
const nonBlankEntities = Array.isArray(entities)
? entities.filter((entity) => entity.fieldValue !== null)
: undefined;
- const searchBounds = getBoundsRoundedToInterval(bounds, contextAggregationInterval, false);
+ const searchBounds = mlTimeBucketsService.getBoundsRoundedToInterval(
+ bounds,
+ contextAggregationInterval,
+ false
+ );
/**
* Loads the full list of annotations for job without any aggs or time boundaries
@@ -138,6 +146,7 @@ export const TimeSeriesChartWithTooltips: FC =
annotationData={annotationData}
bounds={bounds}
detectorIndex={detectorIndex}
+ embeddableMode={embeddableMode}
renderFocusChartOnly={renderFocusChartOnly}
selectedJob={selectedJob}
showAnnotations={showAnnotations}
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts
index cf8e1f0aa989c..30f097dabb8ab 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts
@@ -7,7 +7,7 @@
import { mlJobService } from '../services/job_service';
import { Entity } from './components/entity_control/entity_control';
-import { JobId } from '../../../common/types/anomaly_detection_jobs';
+import type { JobId, CombinedJob } from '../../../common/types/anomaly_detection_jobs';
/**
* Extracts entities from the detector configuration
@@ -15,9 +15,10 @@ import { JobId } from '../../../common/types/anomaly_detection_jobs';
export function getControlsForDetector(
selectedDetectorIndex: number,
selectedEntities: Record,
- selectedJobId: JobId
+ selectedJobId: JobId,
+ job?: CombinedJob
): Entity[] {
- const selectedJob = mlJobService.getJob(selectedJobId);
+ const selectedJob = job ?? mlJobService.getJob(selectedJobId);
const entities: Entity[] = [];
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts
index d0dfdc9ed372b..e6f1a2ec65afd 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts
@@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { lastValueFrom } from 'rxjs';
import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
-import { mlResultsService } from '../services/results_service';
+import { type MlResultsService } from '../services/results_service';
import { ToastNotificationService } from '../services/toast_notification_service';
import { getControlsForDetector } from './get_controls_for_detector';
import { getCriteriaFields } from './get_criteria_fields';
@@ -41,7 +41,8 @@ export const getFunctionDescription = async (
selectedJobId: string;
selectedJob: CombinedJob;
},
- toastNotificationService: ToastNotificationService
+ toastNotificationService: ToastNotificationService,
+ mlResultsService: MlResultsService
) => {
// if the detector's function is metric, fetch the highest scoring anomaly record
// and set to plot the function_description (avg/min/max) of that record by default
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index 757f4cb06543e..ad3f71e5df22d 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -77,6 +77,7 @@ import {
processMetricPlotResults,
processRecordScoreResults,
getFocusData,
+ getTimeseriesexplorerDefaultState,
} from './timeseriesexplorer_utils';
import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings';
import { getControlsForDetector } from './get_controls_for_detector';
@@ -96,46 +97,6 @@ const allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionV
defaultMessage: 'all',
});
-function getTimeseriesexplorerDefaultState() {
- return {
- chartDetails: undefined,
- contextAggregationInterval: undefined,
- contextChartData: undefined,
- contextForecastData: undefined,
- // Not chartable if e.g. model plot with terms for a varp detector
- dataNotChartable: false,
- entitiesLoading: false,
- entityValues: {},
- focusAnnotationData: [],
- focusAggregationInterval: {},
- focusChartData: undefined,
- focusForecastData: undefined,
- fullRefresh: true,
- hasResults: false,
- // Counter to keep track of what data sets have been loaded.
- loadCounter: 0,
- loading: false,
- modelPlotEnabled: false,
- // Toggles display of annotations in the focus chart
- showAnnotations: true,
- showAnnotationsCheckbox: true,
- // Toggles display of forecast data in the focus chart
- showForecast: true,
- showForecastCheckbox: false,
- // Toggles display of model bounds in the focus chart
- showModelBounds: true,
- showModelBoundsCheckbox: false,
- svgWidth: 0,
- tableData: undefined,
- zoomFrom: undefined,
- zoomTo: undefined,
- zoomFromFocusLoaded: undefined,
- zoomToFocusLoaded: undefined,
- chartDataError: undefined,
- sourceIndicesWithGeoFields: {},
- };
-}
-
const containerPadding = 34;
export class TimeSeriesExplorer extends React.Component {
@@ -265,7 +226,7 @@ export class TimeSeriesExplorer extends React.Component {
}
/**
- * Gets focus data for the current component state/
+ * Gets focus data for the current component state
*/
getFocusData(selection) {
const { selectedJobId, selectedForecastId, selectedDetectorIndex, functionDescription } =
@@ -745,7 +706,6 @@ export class TimeSeriesExplorer extends React.Component {
);
}
}
-
// Required to redraw the time series chart when the container is resized.
this.resizeChecker = new ResizeChecker(this.resizeRef.current);
this.resizeChecker.on('resize', () => {
@@ -1091,7 +1051,6 @@ export class TimeSeriesExplorer extends React.Component {
entities={entityControls}
/>
)}
-
{arePartitioningFieldsProvided &&
jobs.length > 0 &&
(fullRefresh === false || loading === false) &&
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts
index d66dca5f565d7..5d13c73f8401f 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts
@@ -17,7 +17,9 @@ export const APP_STATE_ACTION = {
SET_ZOOM: 'SET_ZOOM',
UNSET_ZOOM: 'UNSET_ZOOM',
SET_FUNCTION_DESCRIPTION: 'SET_FUNCTION_DESCRIPTION',
-};
+} as const;
+
+export type TimeseriesexplorerActionType = typeof APP_STATE_ACTION[keyof typeof APP_STATE_ACTION];
export const CHARTS_POINT_TARGET = 500;
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/index.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/index.ts
new file mode 100644
index 0000000000000..b81b4bc96a434
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { TimeSeriesExplorerEmbeddableChart } from './timeseriesexplorer_embeddable_chart';
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_checkbox.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_checkbox.tsx
new file mode 100644
index 0000000000000..e1136e54180ac
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_checkbox.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useMemo } from 'react';
+import { EuiCheckbox, EuiFlexItem, htmlIdGenerator } from '@elastic/eui';
+
+interface Props {
+ id: string;
+ label: string;
+ checked: boolean;
+ onChange: (e: React.ChangeEvent) => void;
+}
+
+export const TimeseriesExplorerCheckbox: FC = ({ id, label, checked, onChange }) => {
+ const checkboxId = useMemo(() => `id-${htmlIdGenerator()()}`, []);
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js
new file mode 100644
index 0000000000000..fd6b0239199bc
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js
@@ -0,0 +1,897 @@
+/*
+ * 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.
+ */
+
+/*
+ * React component for rendering Single Metric Viewer.
+ */
+
+import { isEqual } from 'lodash';
+import moment from 'moment-timezone';
+import { Subject, Subscription, forkJoin } from 'rxjs';
+import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators';
+
+import PropTypes from 'prop-types';
+import React, { Fragment } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { context } from '@kbn/kibana-react-plugin/public';
+
+import {
+ EuiCallOut,
+ EuiCheckbox,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiTitle,
+ EuiTextColor,
+} from '@elastic/eui';
+import { TimeSeriesExplorerHelpPopover } from '../timeseriesexplorer_help_popover';
+
+import {
+ isModelPlotEnabled,
+ isModelPlotChartableForDetector,
+ isSourceDataChartableForDetector,
+} from '../../../../common/util/job_utils';
+
+import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator';
+import { TimeseriesexplorerNoChartData } from '../components/timeseriesexplorer_no_chart_data';
+
+import {
+ APP_STATE_ACTION,
+ CHARTS_POINT_TARGET,
+ TIME_FIELD_NAME,
+} from '../timeseriesexplorer_constants';
+import { getControlsForDetector } from '../get_controls_for_detector';
+import { TimeSeriesChartWithTooltips } from '../components/timeseries_chart/timeseries_chart_with_tooltip';
+import { aggregationTypeTransform } from '@kbn/ml-anomaly-utils';
+import { isMetricDetector } from '../get_function_description';
+import { TimeseriesexplorerChartDataError } from '../components/timeseriesexplorer_chart_data_error';
+import { TimeseriesExplorerCheckbox } from './timeseriesexplorer_checkbox';
+import { timeBucketsServiceFactory } from '../../util/time_buckets_service';
+import { timeSeriesExplorerServiceFactory } from '../../util/time_series_explorer_service';
+import { getTimeseriesexplorerDefaultState } from '../timeseriesexplorer_utils';
+
+// Used to indicate the chart is being plotted across
+// all partition field values, where the cardinality of the field cannot be
+// obtained as it is not aggregatable e.g. 'all distinct kpi_indicator values'
+const allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionValuesLabel', {
+ defaultMessage: 'all',
+});
+
+export class TimeSeriesExplorerEmbeddableChart extends React.Component {
+ static propTypes = {
+ appStateHandler: PropTypes.func.isRequired,
+ autoZoomDuration: PropTypes.number.isRequired,
+ bounds: PropTypes.object.isRequired,
+ chartWidth: PropTypes.number.isRequired,
+ lastRefresh: PropTypes.number.isRequired,
+ previousRefresh: PropTypes.number.isRequired,
+ selectedJobId: PropTypes.string.isRequired,
+ selectedDetectorIndex: PropTypes.number,
+ selectedEntities: PropTypes.object,
+ selectedForecastId: PropTypes.string,
+ zoom: PropTypes.object,
+ toastNotificationService: PropTypes.object,
+ dataViewsService: PropTypes.object,
+ };
+
+ state = getTimeseriesexplorerDefaultState();
+
+ subscriptions = new Subscription();
+
+ unmounted = false;
+
+ /**
+ * Subject for listening brush time range selection.
+ */
+ contextChart$ = new Subject();
+
+ /**
+ * Access ML services in react context.
+ */
+ static contextType = context;
+
+ getBoundsRoundedToInterval;
+ mlTimeSeriesExplorer;
+
+ /**
+ * Returns field names that don't have a selection yet.
+ */
+ getFieldNamesWithEmptyValues = () => {
+ const latestEntityControls = this.getControlsForDetector();
+ return latestEntityControls
+ .filter(({ fieldValue }) => fieldValue === null)
+ .map(({ fieldName }) => fieldName);
+ };
+
+ /**
+ * Checks if all entity control dropdowns have a selection.
+ */
+ arePartitioningFieldsProvided = () => {
+ const fieldNamesWithEmptyValues = this.getFieldNamesWithEmptyValues();
+ return fieldNamesWithEmptyValues.length === 0;
+ };
+
+ toggleShowAnnotationsHandler = () => {
+ this.setState((prevState) => ({
+ showAnnotations: !prevState.showAnnotations,
+ }));
+ };
+
+ toggleShowForecastHandler = () => {
+ this.setState((prevState) => ({
+ showForecast: !prevState.showForecast,
+ }));
+ };
+
+ toggleShowModelBoundsHandler = () => {
+ this.setState({
+ showModelBounds: !this.state.showModelBounds,
+ });
+ };
+
+ setFunctionDescription = (selectedFuction) => {
+ this.props.appStateHandler(APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION, selectedFuction);
+ };
+
+ previousChartProps = {};
+ previousShowAnnotations = undefined;
+ previousShowForecast = undefined;
+ previousShowModelBounds = undefined;
+
+ tableFilter = (field, value, operator) => {
+ const entities = this.getControlsForDetector();
+ const entity = entities.find(({ fieldName }) => fieldName === field);
+
+ if (entity === undefined) {
+ return;
+ }
+
+ const { appStateHandler } = this.props;
+
+ let resultValue = '';
+ if (operator === '+' && entity.fieldValue !== value) {
+ resultValue = value;
+ } else if (operator === '-' && entity.fieldValue === value) {
+ resultValue = null;
+ } else {
+ return;
+ }
+
+ const resultEntities = {
+ ...entities.reduce((appStateEntities, appStateEntity) => {
+ appStateEntities[appStateEntity.fieldName] = appStateEntity.fieldValue;
+ return appStateEntities;
+ }, {}),
+ [entity.fieldName]: resultValue,
+ };
+
+ appStateHandler(APP_STATE_ACTION.SET_ENTITIES, resultEntities);
+ };
+
+ contextChartSelectedInitCallDone = false;
+
+ getFocusAggregationInterval(selection) {
+ const { selectedJob } = this.props;
+
+ // Calculate the aggregation interval for the focus chart.
+ const bounds = { min: moment(selection.from), max: moment(selection.to) };
+
+ return this.mlTimeSeriesExplorer.calculateAggregationInterval(
+ bounds,
+ CHARTS_POINT_TARGET,
+ selectedJob
+ );
+ }
+
+ /**
+ * Gets focus data for the current component state
+ */
+ getFocusData(selection) {
+ const { selectedForecastId, selectedDetectorIndex, functionDescription, selectedJob } =
+ this.props;
+ const { modelPlotEnabled } = this.state;
+ if (isMetricDetector(selectedJob, selectedDetectorIndex) && functionDescription === undefined) {
+ return;
+ }
+ const entityControls = this.getControlsForDetector();
+
+ // Calculate the aggregation interval for the focus chart.
+ const bounds = { min: moment(selection.from), max: moment(selection.to) };
+ const focusAggregationInterval = this.getFocusAggregationInterval(selection);
+
+ // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete.
+ // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected
+ // to some extent with all detector functions if not searching complete buckets.
+ const searchBounds = this.getBoundsRoundedToInterval(bounds, focusAggregationInterval, false);
+
+ return this.mlTimeSeriesExplorer.getFocusData(
+ this.getCriteriaFields(selectedDetectorIndex, entityControls),
+ selectedDetectorIndex,
+ focusAggregationInterval,
+ selectedForecastId,
+ modelPlotEnabled,
+ entityControls.filter((entity) => entity.fieldValue !== null),
+ searchBounds,
+ selectedJob,
+ functionDescription,
+ TIME_FIELD_NAME
+ );
+ }
+
+ contextChartSelected = (selection) => {
+ const zoomState = {
+ from: selection.from.toISOString(),
+ to: selection.to.toISOString(),
+ };
+
+ if (
+ isEqual(this.props.zoom, zoomState) &&
+ this.state.focusChartData !== undefined &&
+ this.props.previousRefresh === this.props.lastRefresh
+ ) {
+ return;
+ }
+
+ this.contextChart$.next(selection);
+ this.props.appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState);
+ };
+
+ setForecastId = (forecastId) => {
+ this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId);
+ };
+
+ displayErrorToastMessages = (error, errorMsg) => {
+ if (this.props.toastNotificationService) {
+ this.props.toastNotificationService.displayErrorToast(error, errorMsg, 2000);
+ }
+ this.setState({ loading: false, chartDataError: errorMsg });
+ };
+
+ loadSingleMetricData = (fullRefresh = true) => {
+ const {
+ autoZoomDuration,
+ bounds,
+ selectedDetectorIndex,
+ zoom,
+ functionDescription,
+ selectedJob,
+ } = this.props;
+
+ const { loadCounter: currentLoadCounter } = this.state;
+ if (selectedJob === undefined) {
+ return;
+ }
+ if (isMetricDetector(selectedJob, selectedDetectorIndex) && functionDescription === undefined) {
+ return;
+ }
+
+ const functionToPlotByIfMetric = aggregationTypeTransform.toES(functionDescription);
+
+ this.contextChartSelectedInitCallDone = false;
+
+ // Only when `fullRefresh` is true we'll reset all data
+ // and show the loading spinner within the page.
+ const entityControls = this.getControlsForDetector();
+ this.setState(
+ {
+ fullRefresh,
+ loadCounter: currentLoadCounter + 1,
+ loading: true,
+ chartDataError: undefined,
+ ...(fullRefresh
+ ? {
+ chartDetails: undefined,
+ contextChartData: undefined,
+ contextForecastData: undefined,
+ focusChartData: undefined,
+ focusForecastData: undefined,
+ modelPlotEnabled:
+ isModelPlotChartableForDetector(selectedJob, selectedDetectorIndex) &&
+ isModelPlotEnabled(selectedJob, selectedDetectorIndex, entityControls),
+ hasResults: false,
+ dataNotChartable: false,
+ }
+ : {}),
+ },
+ () => {
+ const { loadCounter, modelPlotEnabled } = this.state;
+ const { selectedJob } = this.props;
+
+ const detectorIndex = selectedDetectorIndex;
+
+ let awaitingCount = 3;
+
+ const stateUpdate = {};
+
+ // finish() function, called after each data set has been loaded and processed.
+ // The last one to call it will trigger the page render.
+ const finish = (counterVar) => {
+ awaitingCount--;
+ if (awaitingCount === 0 && counterVar === loadCounter) {
+ stateUpdate.hasResults =
+ (Array.isArray(stateUpdate.contextChartData) &&
+ stateUpdate.contextChartData.length > 0) ||
+ (Array.isArray(stateUpdate.contextForecastData) &&
+ stateUpdate.contextForecastData.length > 0);
+ stateUpdate.loading = false;
+
+ // Set zoomFrom/zoomTo attributes in scope which will result in the metric chart automatically
+ // selecting the specified range in the context chart, and so loading that date range in the focus chart.
+ // Only touch the zoom range if data for the context chart has been loaded and all necessary
+ // partition fields have a selection.
+ if (
+ stateUpdate.contextChartData.length &&
+ this.arePartitioningFieldsProvided() === true
+ ) {
+ // Check for a zoom parameter in the appState (URL).
+ let focusRange = this.mlTimeSeriesExplorer.calculateInitialFocusRange(
+ zoom,
+ stateUpdate.contextAggregationInterval,
+ bounds
+ );
+ if (
+ focusRange === undefined ||
+ this.previousSelectedForecastId !== this.props.selectedForecastId
+ ) {
+ focusRange = this.mlTimeSeriesExplorer.calculateDefaultFocusRange(
+ autoZoomDuration,
+ stateUpdate.contextAggregationInterval,
+ stateUpdate.contextChartData,
+ stateUpdate.contextForecastData
+ );
+ this.previousSelectedForecastId = this.props.selectedForecastId;
+ }
+
+ this.contextChartSelected({
+ from: focusRange[0],
+ to: focusRange[1],
+ });
+ }
+
+ this.setState(stateUpdate);
+ }
+ };
+
+ const nonBlankEntities = entityControls.filter((entity) => {
+ return entity.fieldValue !== null;
+ });
+
+ if (
+ modelPlotEnabled === false &&
+ isSourceDataChartableForDetector(selectedJob, detectorIndex) === false &&
+ nonBlankEntities.length > 0
+ ) {
+ // For detectors where model plot has been enabled with a terms filter and the
+ // selected entity(s) are not in the terms list, indicate that data cannot be viewed.
+ stateUpdate.hasResults = false;
+ stateUpdate.loading = false;
+ stateUpdate.dataNotChartable = true;
+ this.setState(stateUpdate);
+ return;
+ }
+
+ // Calculate the aggregation interval for the context chart.
+ // Context chart swimlane will display bucket anomaly score at the same interval.
+ stateUpdate.contextAggregationInterval =
+ this.mlTimeSeriesExplorer.calculateAggregationInterval(
+ bounds,
+ CHARTS_POINT_TARGET,
+ selectedJob
+ );
+
+ // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete.
+ // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected
+ // to some extent with all detector functions if not searching complete buckets.
+ const searchBounds = this.getBoundsRoundedToInterval(
+ bounds,
+ stateUpdate.contextAggregationInterval,
+ false
+ );
+
+ // Query 1 - load metric data at low granularity across full time range.
+ // Pass a counter flag into the finish() function to make sure we only process the results
+ // for the most recent call to the load the data in cases where the job selection and time filter
+ // have been altered in quick succession (such as from the job picker with 'Apply time range').
+ const counter = loadCounter;
+ this.context.services.mlServices.mlTimeSeriesSearchService
+ .getMetricData(
+ selectedJob,
+ detectorIndex,
+ nonBlankEntities,
+ searchBounds.min.valueOf(),
+ searchBounds.max.valueOf(),
+ stateUpdate.contextAggregationInterval.asMilliseconds(),
+ functionToPlotByIfMetric
+ )
+ .toPromise()
+ .then((resp) => {
+ const fullRangeChartData = this.mlTimeSeriesExplorer.processMetricPlotResults(
+ resp.results,
+ modelPlotEnabled
+ );
+ stateUpdate.contextChartData = fullRangeChartData;
+ finish(counter);
+ })
+ .catch((err) => {
+ const errorMsg = i18n.translate('xpack.ml.timeSeriesExplorer.metricDataErrorMessage', {
+ defaultMessage: 'Error getting metric data',
+ });
+ this.displayErrorToastMessages(err, errorMsg);
+ });
+
+ // Query 2 - load max record score at same granularity as context chart
+ // across full time range for use in the swimlane.
+ this.context.services.mlServices.mlResultsService
+ .getRecordMaxScoreByTime(
+ selectedJob.job_id,
+ this.getCriteriaFields(detectorIndex, entityControls),
+ searchBounds.min.valueOf(),
+ searchBounds.max.valueOf(),
+ stateUpdate.contextAggregationInterval.asMilliseconds(),
+ functionToPlotByIfMetric
+ )
+ .then((resp) => {
+ const fullRangeRecordScoreData = this.mlTimeSeriesExplorer.processRecordScoreResults(
+ resp.results
+ );
+ stateUpdate.swimlaneData = fullRangeRecordScoreData;
+ finish(counter);
+ })
+ .catch((err) => {
+ const errorMsg = i18n.translate(
+ 'xpack.ml.timeSeriesExplorer.bucketAnomalyScoresErrorMessage',
+ {
+ defaultMessage: 'Error getting bucket anomaly scores',
+ }
+ );
+
+ this.displayErrorToastMessages(err, errorMsg);
+ });
+
+ // Query 3 - load details on the chart used in the chart title (charting function and entity(s)).
+ this.context.services.mlServices.mlTimeSeriesSearchService
+ .getChartDetails(
+ selectedJob,
+ detectorIndex,
+ entityControls,
+ searchBounds.min.valueOf(),
+ searchBounds.max.valueOf()
+ )
+ .then((resp) => {
+ stateUpdate.chartDetails = resp.results;
+ finish(counter);
+ })
+ .catch((err) => {
+ this.displayErrorToastMessages(
+ err,
+ i18n.translate('xpack.ml.timeSeriesExplorer.entityCountsErrorMessage', {
+ defaultMessage: 'Error getting entity counts',
+ })
+ );
+ });
+ }
+ );
+ };
+
+ /**
+ * Updates local state of detector related controls from the global state.
+ * @param callback to invoke after a state update.
+ */
+ getControlsForDetector = () => {
+ const { selectedDetectorIndex, selectedEntities, selectedJobId, selectedJob } = this.props;
+ return getControlsForDetector(
+ selectedDetectorIndex,
+ selectedEntities,
+ selectedJobId,
+ selectedJob
+ );
+ };
+
+ /**
+ * Updates criteria fields for API calls, e.g. getAnomaliesTableData
+ * @param detectorIndex
+ * @param entities
+ */
+ getCriteriaFields(detectorIndex, entities) {
+ // Only filter on the entity if the field has a value.
+ const nonBlankEntities = entities.filter((entity) => entity.fieldValue !== null);
+ return [
+ {
+ fieldName: 'detector_index',
+ fieldValue: detectorIndex,
+ },
+ ...nonBlankEntities,
+ ];
+ }
+
+ async componentDidMount() {
+ this.getBoundsRoundedToInterval = timeBucketsServiceFactory(
+ this.context.services.uiSettings
+ ).getBoundsRoundedToInterval;
+
+ this.mlTimeSeriesExplorer = timeSeriesExplorerServiceFactory(
+ this.context.services.uiSettings,
+ this.context.services.mlServices.mlApiServices,
+ this.context.services.mlServices.mlResultsService
+ );
+
+ // Listen for context chart updates.
+ this.subscriptions.add(
+ this.contextChart$
+ .pipe(
+ tap((selection) => {
+ this.setState({
+ zoomFrom: selection.from,
+ zoomTo: selection.to,
+ });
+ }),
+ debounceTime(500),
+ tap((selection) => {
+ const {
+ contextChartData,
+ contextForecastData,
+ focusChartData,
+ zoomFromFocusLoaded,
+ zoomToFocusLoaded,
+ } = this.state;
+
+ if (
+ (contextChartData === undefined || contextChartData.length === 0) &&
+ (contextForecastData === undefined || contextForecastData.length === 0)
+ ) {
+ return;
+ }
+
+ if (
+ (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) ||
+ zoomFromFocusLoaded.getTime() !== selection.from.getTime() ||
+ zoomToFocusLoaded.getTime() !== selection.to.getTime()
+ ) {
+ this.contextChartSelectedInitCallDone = true;
+
+ this.setState({
+ loading: true,
+ fullRefresh: false,
+ });
+ }
+ }),
+ switchMap((selection) => {
+ return forkJoin([this.getFocusData(selection)]);
+ }),
+ withLatestFrom(this.contextChart$)
+ )
+ .subscribe(([[refreshFocusData, tableData], selection]) => {
+ const { modelPlotEnabled } = this.state;
+
+ // All the data is ready now for a state update.
+ this.setState({
+ focusAggregationInterval: this.getFocusAggregationInterval({
+ from: selection.from,
+ to: selection.to,
+ }),
+ loading: false,
+ showModelBoundsCheckbox: modelPlotEnabled && refreshFocusData.focusChartData.length > 0,
+ zoomFromFocusLoaded: selection.from,
+ zoomToFocusLoaded: selection.to,
+ ...refreshFocusData,
+ ...tableData,
+ });
+ })
+ );
+
+ if (this.context && this.props.selectedJob !== undefined) {
+ // Populate the map of jobs / detectors / field formatters for the selected IDs and refresh.
+ this.context.services.mlServices.mlFieldFormatService.populateFormats([
+ this.props.selectedJob.job_id,
+ ]);
+ }
+
+ this.componentDidUpdate();
+ }
+
+ componentDidUpdate(previousProps) {
+ if (
+ previousProps === undefined ||
+ previousProps.selectedForecastId !== this.props.selectedForecastId
+ ) {
+ if (this.props.selectedForecastId !== undefined) {
+ // Ensure the forecast data will be shown if hidden previously.
+ this.setState({ showForecast: true });
+ // Not best practice but we need the previous value for another comparison
+ // once all the data was loaded.
+ if (previousProps !== undefined) {
+ this.previousSelectedForecastId = previousProps.selectedForecastId;
+ }
+ }
+ }
+
+ if (
+ previousProps === undefined ||
+ !isEqual(previousProps.bounds, this.props.bounds) ||
+ (!isEqual(previousProps.lastRefresh, this.props.lastRefresh) &&
+ previousProps.lastRefresh !== 0) ||
+ !isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
+ !isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
+ previousProps.selectedForecastId !== this.props.selectedForecastId ||
+ previousProps.selectedJobId !== this.props.selectedJobId ||
+ previousProps.functionDescription !== this.props.functionDescription
+ ) {
+ const fullRefresh =
+ previousProps === undefined ||
+ !isEqual(previousProps.bounds, this.props.bounds) ||
+ !isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
+ !isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
+ previousProps.selectedForecastId !== this.props.selectedForecastId ||
+ previousProps.selectedJobId !== this.props.selectedJobId ||
+ previousProps.functionDescription !== this.props.functionDescription;
+ this.loadSingleMetricData(fullRefresh);
+ }
+
+ if (previousProps === undefined) {
+ return;
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscriptions.unsubscribe();
+ this.unmounted = true;
+ }
+
+ render() {
+ const {
+ autoZoomDuration,
+ bounds,
+ chartWidth,
+ lastRefresh,
+ selectedDetectorIndex,
+ selectedJob,
+ } = this.props;
+
+ const {
+ chartDetails,
+ contextAggregationInterval,
+ contextChartData,
+ contextForecastData,
+ dataNotChartable,
+ focusAggregationInterval,
+ focusAnnotationData,
+ focusChartData,
+ focusForecastData,
+ fullRefresh,
+ hasResults,
+ loading,
+ modelPlotEnabled,
+ showAnnotations,
+ showAnnotationsCheckbox,
+ showForecast,
+ showForecastCheckbox,
+ showModelBounds,
+ showModelBoundsCheckbox,
+ swimlaneData,
+ zoomFrom,
+ zoomTo,
+ zoomFromFocusLoaded,
+ zoomToFocusLoaded,
+ chartDataError,
+ } = this.state;
+ const chartProps = {
+ modelPlotEnabled,
+ contextChartData,
+ contextChartSelected: this.contextChartSelected,
+ contextForecastData,
+ contextAggregationInterval,
+ swimlaneData,
+ focusAnnotationData,
+ focusChartData,
+ focusForecastData,
+ focusAggregationInterval,
+ svgWidth: chartWidth,
+ zoomFrom,
+ zoomTo,
+ zoomFromFocusLoaded,
+ zoomToFocusLoaded,
+ autoZoomDuration,
+ };
+
+ const entityControls = this.getControlsForDetector();
+ const fieldNamesWithEmptyValues = this.getFieldNamesWithEmptyValues();
+ const arePartitioningFieldsProvided = this.arePartitioningFieldsProvided();
+
+ let renderFocusChartOnly = true;
+
+ if (
+ isEqual(this.previousChartProps.focusForecastData, chartProps.focusForecastData) &&
+ isEqual(this.previousChartProps.focusChartData, chartProps.focusChartData) &&
+ isEqual(this.previousChartProps.focusAnnotationData, chartProps.focusAnnotationData) &&
+ this.previousShowForecast === showForecast &&
+ this.previousShowModelBounds === showModelBounds &&
+ this.props.previousRefresh === lastRefresh
+ ) {
+ renderFocusChartOnly = false;
+ }
+
+ this.previousChartProps = chartProps;
+ this.previousShowForecast = showForecast;
+ this.previousShowModelBounds = showModelBounds;
+
+ return (
+ <>
+ {fieldNamesWithEmptyValues.length > 0 && (
+ <>
+
+ }
+ iconType="help"
+ size="s"
+ />
+
+ >
+ )}
+
+ {fullRefresh && loading === true && (
+
+ )}
+
+ {loading === false && chartDataError !== undefined && (
+
+ )}
+
+ {arePartitioningFieldsProvided &&
+ selectedJob &&
+ (fullRefresh === false || loading === false) &&
+ hasResults === false &&
+ chartDataError === undefined && (
+
+ )}
+ {arePartitioningFieldsProvided &&
+ selectedJob &&
+ (fullRefresh === false || loading === false) &&
+ hasResults === true && (
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.ml.timeSeriesExplorer.singleTimeSeriesAnalysisTitle',
+ {
+ defaultMessage: 'Single time series analysis of {functionLabel}',
+ values: { functionLabel: chartDetails.functionLabel },
+ }
+ )}
+
+
+ {chartDetails.entityData.count === 1 && (
+
+ {chartDetails.entityData.entities.length > 0 && '('}
+ {chartDetails.entityData.entities
+ .map((entity) => {
+ return `${entity.fieldName}: ${entity.fieldValue}`;
+ })
+ .join(', ')}
+ {chartDetails.entityData.entities.length > 0 && ')'}
+
+ )}
+ {chartDetails.entityData.count !== 1 && (
+
+ {chartDetails.entityData.entities.map((countData, i) => {
+ return (
+
+ {i18n.translate(
+ 'xpack.ml.timeSeriesExplorer.countDataInChartDetailsDescription',
+ {
+ defaultMessage:
+ '{openBrace}{cardinalityValue} distinct {fieldName} {cardinality, plural, one {} other { values}}{closeBrace}',
+ values: {
+ openBrace: i === 0 ? '(' : '',
+ closeBrace:
+ i === chartDetails.entityData.entities.length - 1
+ ? ')'
+ : '',
+ cardinalityValue:
+ countData.cardinality === 0
+ ? allValuesLabel
+ : countData.cardinality,
+ cardinality: countData.cardinality,
+ fieldName: countData.fieldName,
+ },
+ }
+ )}
+ {i !== chartDetails.entityData.entities.length - 1 ? ', ' : ''}
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+
+
+
+
+ {showModelBoundsCheckbox && (
+
+ )}
+
+ {showAnnotationsCheckbox && (
+
+ )}
+
+ {showForecastCheckbox && (
+
+
+ {i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', {
+ defaultMessage: 'show forecast',
+ })}
+
+ }
+ checked={showForecast}
+ onChange={this.toggleShowForecastHandler}
+ />
+
+ )}
+
+
+
+
+ )}
+ >
+ );
+ }
+}
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx
index afd93fd5acee1..3557523c113fc 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx
@@ -5,12 +5,14 @@
* 2.0.
*/
-import React from 'react';
+import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { HelpPopover } from '../components/help_popover/help_popover';
-export const TimeSeriesExplorerHelpPopover = () => {
+export const TimeSeriesExplorerHelpPopover: FC<{ embeddableMode: boolean }> = ({
+ embeddableMode,
+}) => {
return (
{
defaultMessage="If you create a forecast, predicted data values are added to the chart. A shaded area around these values represents the confidence level; as you forecast further into the future, the confidence level generally decreases."
/>
-
-
-
+ {!embeddableMode && (
+
+
+
+ )}
{
+ if (
+ isModelPlotChartableForDetector(job, detectorIndex) &&
+ isModelPlotEnabled(job, detectorIndex, entityFields)
+ ) {
+ // Extract the partition, by, over fields on which to filter.
+ const criteriaFields = [];
+ const detector = job.analysis_config.detectors[detectorIndex];
+ if (detector.partition_field_name !== undefined) {
+ const partitionEntity: any = find(entityFields, {
+ fieldName: detector.partition_field_name,
+ });
+ if (partitionEntity !== undefined) {
+ criteriaFields.push(
+ { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName },
+ { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }
+ );
+ }
+ }
+
+ if (detector.over_field_name !== undefined) {
+ const overEntity: any = find(entityFields, { fieldName: detector.over_field_name });
+ if (overEntity !== undefined) {
+ criteriaFields.push(
+ { fieldName: 'over_field_name', fieldValue: overEntity.fieldName },
+ { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }
+ );
+ }
+ }
+
+ if (detector.by_field_name !== undefined) {
+ const byEntity: any = find(entityFields, { fieldName: detector.by_field_name });
+ if (byEntity !== undefined) {
+ criteriaFields.push(
+ { fieldName: 'by_field_name', fieldValue: byEntity.fieldName },
+ { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }
+ );
+ }
+ }
+
+ return mlResultsService.getModelPlotOutput(
+ job.job_id,
+ detectorIndex,
+ criteriaFields,
+ earliestMs,
+ latestMs,
+ intervalMs
+ );
+ } else {
+ const obj: ModelPlotOutput = {
+ success: true,
+ results: {},
+ };
+
+ const chartConfig = buildConfigFromDetector(job, detectorIndex);
+
+ return mlResultsService
+ .getMetricData(
+ chartConfig.datafeedConfig.indices.join(','),
+ entityFields,
+ chartConfig.datafeedConfig.query,
+ esMetricFunction ?? chartConfig.metricFunction,
+ chartConfig.metricFieldName,
+ chartConfig.summaryCountFieldName,
+ chartConfig.timeField,
+ earliestMs,
+ latestMs,
+ intervalMs,
+ chartConfig?.datafeedConfig
+ )
+ .pipe(
+ map((resp) => {
+ each(resp.results, (value, time) => {
+ // @ts-ignore
+ obj.results[time] = {
+ actual: value,
+ };
+ });
+ return obj;
+ })
+ );
+ }
+ },
+ // Builds chart detail information (charting function description and entity counts) used
+ // in the title area of the time series chart.
+ // Queries Elasticsearch if necessary to obtain the distinct count of entities
+ // for which data is being plotted.
+ getChartDetails(
+ job: Job,
+ detectorIndex: number,
+ entityFields: any[],
+ earliestMs: number,
+ latestMs: number
+ ) {
+ return new Promise((resolve, reject) => {
+ const obj: any = {
+ success: true,
+ results: { functionLabel: '', entityData: { entities: [] } },
+ };
+
+ const chartConfig = buildConfigFromDetector(job, detectorIndex);
+ let functionLabel: string | null = chartConfig.metricFunction;
+ if (chartConfig.metricFieldName !== undefined) {
+ functionLabel += ' ';
+ functionLabel += chartConfig.metricFieldName;
+ }
+ obj.results.functionLabel = functionLabel;
+
+ const blankEntityFields = filter(entityFields, (entity) => {
+ return entity.fieldValue === null;
+ });
+
+ // Look to see if any of the entity fields have defined values
+ // (i.e. blank input), and if so obtain the cardinality.
+ if (blankEntityFields.length === 0) {
+ obj.results.entityData.count = 1;
+ obj.results.entityData.entities = entityFields;
+ resolve(obj);
+ } else {
+ const entityFieldNames: string[] = blankEntityFields.map((f) => f.fieldName);
+ mlApiServices
+ .getCardinalityOfFields({
+ index: chartConfig.datafeedConfig.indices.join(','),
+ fieldNames: entityFieldNames,
+ query: chartConfig.datafeedConfig.query,
+ timeFieldName: chartConfig.timeField,
+ earliestMs,
+ latestMs,
+ })
+ .then((results: any) => {
+ each(blankEntityFields, (field) => {
+ // results will not contain keys for non-aggregatable fields,
+ // so store as 0 to indicate over all field values.
+ obj.results.entityData.entities.push({
+ fieldName: field.fieldName,
+ cardinality: get(results, field.fieldName, 0),
+ });
+ });
+
+ resolve(obj);
+ })
+ .catch((resp: any) => {
+ reject(resp);
+ });
+ }
+ });
+ },
+ };
+}
+
+export type MlTimeSeriesSeachService = ReturnType;
diff --git a/x-pack/plugins/ml/public/application/util/index_service.ts b/x-pack/plugins/ml/public/application/util/index_service.ts
new file mode 100644
index 0000000000000..f4b6a1fc13d77
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/util/index_service.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
+import type { Job } from '../../../common/types/anomaly_detection_jobs';
+
+// TODO Consolidate with legacy code in `ml/public/application/util/index_utils.ts`.
+export function indexServiceFactory(dataViewsService: DataViewsContract) {
+ return {
+ /**
+ * Retrieves the data view ID from the given name.
+ * If a job is passed in, a temporary data view will be created if the requested data view doesn't exist.
+ * @param name - The name or index pattern of the data view.
+ * @param job - Optional job object.
+ * @returns The data view ID or null if it doesn't exist.
+ */
+ async getDataViewIdFromName(name: string, job?: Job): Promise {
+ if (dataViewsService === null) {
+ throw new Error('Data views are not initialized!');
+ }
+ const dataViews = await dataViewsService.find(name);
+ const dataView = dataViews.find((dv) => dv.getIndexPattern() === name);
+ if (!dataView) {
+ if (job !== undefined) {
+ const tempDataView = await dataViewsService.create({
+ id: undefined,
+ name,
+ title: name,
+ timeFieldName: job.data_description.time_field!,
+ });
+ return tempDataView.id ?? null;
+ }
+ return null;
+ }
+ return dataView.id ?? dataView.getIndexPattern();
+ },
+ getDataViewById(id: string): Promise {
+ if (dataViewsService === null) {
+ throw new Error('Data views are not initialized!');
+ }
+
+ if (id) {
+ return dataViewsService.get(id);
+ } else {
+ return dataViewsService.create({});
+ }
+ },
+ };
+}
+
+export type MlIndexUtils = ReturnType;
diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts
index 9a5410918a099..0f413ed9c2c71 100644
--- a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts
+++ b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts
@@ -33,6 +33,7 @@ export declare class TimeBuckets {
public setBounds(bounds: TimeRangeBounds): void;
public getBounds(): { min: any; max: any };
public getInterval(): TimeBucketsInterval;
+ public getIntervalToNearestMultiple(divisorSecs: any): TimeBucketsInterval;
public getScaledDateFormat(): string;
}
diff --git a/x-pack/plugins/ml/public/application/util/time_buckets_service.ts b/x-pack/plugins/ml/public/application/util/time_buckets_service.ts
new file mode 100644
index 0000000000000..480f279a603b1
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/util/time_buckets_service.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { useMemo } from 'react';
+import type { IUiSettingsClient } from '@kbn/core/public';
+import { UI_SETTINGS } from '@kbn/data-plugin/public';
+import moment from 'moment';
+import { type TimeRangeBounds, type TimeBucketsInterval, TimeBuckets } from './time_buckets';
+import { useMlKibana } from '../contexts/kibana';
+
+// TODO Consolidate with legacy code in `ml/public/application/util/time_buckets.js`.
+export function timeBucketsServiceFactory(uiSettings: IUiSettingsClient) {
+ function getTimeBuckets(): InstanceType {
+ return new TimeBuckets({
+ [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
+ [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
+ dateFormat: uiSettings.get('dateFormat'),
+ 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
+ });
+ }
+ function getBoundsRoundedToInterval(
+ bounds: TimeRangeBounds,
+ interval: TimeBucketsInterval,
+ inclusiveEnd: boolean = false
+ ): Required {
+ // Returns new bounds, created by flooring the min of the provided bounds to the start of
+ // the specified interval (a moment duration), and rounded upwards (Math.ceil) to 1ms before
+ // the start of the next interval (Kibana dashboards search >= bounds min, and <= bounds max,
+ // so we subtract 1ms off the max to avoid querying start of the new Elasticsearch aggregation bucket).
+ const intervalMs = interval.asMilliseconds();
+ const adjustedMinMs = Math.floor(bounds.min!.valueOf() / intervalMs) * intervalMs;
+ let adjustedMaxMs = Math.ceil(bounds.max!.valueOf() / intervalMs) * intervalMs;
+
+ // Don't include the start ms of the next bucket unless specified..
+ if (inclusiveEnd === false) {
+ adjustedMaxMs = adjustedMaxMs - 1;
+ }
+ return { min: moment(adjustedMinMs), max: moment(adjustedMaxMs) };
+ }
+
+ return { getTimeBuckets, getBoundsRoundedToInterval };
+}
+
+export type TimeBucketsService = ReturnType;
+
+export function useTimeBucketsService(): TimeBucketsService {
+ const {
+ services: { uiSettings },
+ } = useMlKibana();
+
+ const mlTimeBucketsService = useMemo(() => timeBucketsServiceFactory(uiSettings), [uiSettings]);
+ return mlTimeBucketsService;
+}
diff --git a/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts
new file mode 100644
index 0000000000000..4af8d98093cbd
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts
@@ -0,0 +1,648 @@
+/*
+ * 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 { useMemo } from 'react';
+import type { IUiSettingsClient } from '@kbn/core/public';
+import { aggregationTypeTransform } from '@kbn/ml-anomaly-utils';
+import { isMultiBucketAnomaly, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
+import { extractErrorMessage } from '@kbn/ml-error-utils';
+import moment from 'moment';
+import { forkJoin, Observable, of } from 'rxjs';
+import { each, get } from 'lodash';
+import { catchError, map } from 'rxjs/operators';
+import { type MlAnomalyRecordDoc } from '@kbn/ml-anomaly-utils';
+import { parseInterval } from '../../../common/util/parse_interval';
+import type { GetAnnotationsResponse } from '../../../common/types/annotations';
+import { mlFunctionToESAggregation } from '../../../common/util/job_utils';
+import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search';
+import { CHARTS_POINT_TARGET } from '../timeseriesexplorer/timeseriesexplorer_constants';
+import { timeBucketsServiceFactory } from './time_buckets_service';
+import type { TimeRangeBounds } from './time_buckets';
+import type { Job } from '../../../common/types/anomaly_detection_jobs';
+import type { TimeBucketsInterval } from './time_buckets';
+import type {
+ ChartDataPoint,
+ FocusData,
+ Interval,
+} from '../timeseriesexplorer/timeseriesexplorer_utils/get_focus_data';
+import type { CriteriaField } from '../services/results_service';
+import {
+ MAX_SCHEDULED_EVENTS,
+ TIME_FIELD_NAME,
+} from '../timeseriesexplorer/timeseriesexplorer_constants';
+import type { MlApiServices } from '../services/ml_api_service';
+import { mlResultsServiceProvider, type MlResultsService } from '../services/results_service';
+import { forecastServiceProvider } from '../services/forecast_service_provider';
+import { timeSeriesSearchServiceFactory } from '../timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service';
+import { useMlKibana } from '../contexts/kibana';
+
+// TODO Consolidate with legacy code in
+// `ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js`.
+export function timeSeriesExplorerServiceFactory(
+ uiSettings: IUiSettingsClient,
+ mlApiServices: MlApiServices,
+ mlResultsService: MlResultsService
+) {
+ const timeBuckets = timeBucketsServiceFactory(uiSettings);
+ const mlForecastService = forecastServiceProvider(mlApiServices);
+ const mlTimeSeriesSearchService = timeSeriesSearchServiceFactory(mlResultsService, mlApiServices);
+
+ function getAutoZoomDuration(selectedJob: Job) {
+ // Calculate the 'auto' zoom duration which shows data at bucket span granularity.
+ // Get the minimum bucket span of selected jobs.
+ let autoZoomDuration;
+ if (selectedJob.analysis_config.bucket_span) {
+ const bucketSpan = parseInterval(selectedJob.analysis_config.bucket_span);
+ const bucketSpanSeconds = bucketSpan!.asSeconds();
+
+ // In most cases the duration can be obtained by simply multiplying the points target
+ // Check that this duration returns the bucket span when run back through the
+ // TimeBucket interval calculation.
+ autoZoomDuration = bucketSpanSeconds * 1000 * (CHARTS_POINT_TARGET - 1);
+
+ // Use a maxBars of 10% greater than the target.
+ const maxBars = Math.floor(1.1 * CHARTS_POINT_TARGET);
+ const buckets = timeBuckets.getTimeBuckets();
+ buckets.setInterval('auto');
+ buckets.setBarTarget(Math.floor(CHARTS_POINT_TARGET));
+ buckets.setMaxBars(maxBars);
+
+ // Set bounds from 'now' for testing the auto zoom duration.
+ const nowMs = new Date().getTime();
+ const max = moment(nowMs);
+ const min = moment(nowMs - autoZoomDuration);
+ buckets.setBounds({ min, max });
+
+ const calculatedInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds);
+ const calculatedIntervalSecs = calculatedInterval.asSeconds();
+ if (calculatedIntervalSecs !== bucketSpanSeconds) {
+ // If we haven't got the span back, which may occur depending on the 'auto' ranges
+ // used in TimeBuckets and the bucket span of the job, then multiply by the ratio
+ // of the bucket span to the calculated interval.
+ autoZoomDuration = autoZoomDuration * (bucketSpanSeconds / calculatedIntervalSecs);
+ }
+ }
+
+ return autoZoomDuration;
+ }
+
+ function calculateAggregationInterval(
+ bounds: TimeRangeBounds,
+ bucketsTarget: number | undefined,
+ selectedJob: Job
+ ) {
+ // Aggregation interval used in queries should be a function of the time span of the chart
+ // and the bucket span of the selected job(s).
+ const barTarget = bucketsTarget !== undefined ? bucketsTarget : 100;
+ // Use a maxBars of 10% greater than the target.
+ const maxBars = Math.floor(1.1 * barTarget);
+ const buckets = timeBuckets.getTimeBuckets();
+ buckets.setInterval('auto');
+ buckets.setBounds(bounds);
+ buckets.setBarTarget(Math.floor(barTarget));
+ buckets.setMaxBars(maxBars);
+ let aggInterval;
+
+ if (selectedJob.analysis_config.bucket_span) {
+ // Ensure the aggregation interval is always a multiple of the bucket span to avoid strange
+ // behaviour such as adjacent chart buckets holding different numbers of job results.
+ const bucketSpan = parseInterval(selectedJob.analysis_config.bucket_span);
+ const bucketSpanSeconds = bucketSpan!.asSeconds();
+ aggInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds);
+
+ // Set the interval back to the job bucket span if the auto interval is smaller.
+ const secs = aggInterval.asSeconds();
+ if (secs < bucketSpanSeconds) {
+ buckets.setInterval(bucketSpanSeconds + 's');
+ aggInterval = buckets.getInterval();
+ }
+ }
+
+ return aggInterval;
+ }
+
+ function calculateInitialFocusRange(
+ zoomState: any,
+ contextAggregationInterval: any,
+ bounds: TimeRangeBounds
+ ) {
+ if (zoomState !== undefined) {
+ // Check that the zoom times are valid.
+ // zoomFrom must be at or after context chart search bounds earliest,
+ // zoomTo must be at or before context chart search bounds latest.
+ const zoomFrom = moment(zoomState.from, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true);
+ const zoomTo = moment(zoomState.to, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true);
+ const searchBounds = timeBuckets.getBoundsRoundedToInterval(
+ bounds,
+ contextAggregationInterval,
+ true
+ );
+ const earliest = searchBounds.min;
+ const latest = searchBounds.max;
+
+ if (
+ zoomFrom.isValid() &&
+ zoomTo.isValid() &&
+ zoomTo.isAfter(zoomFrom) &&
+ zoomFrom.isBetween(earliest, latest, null, '[]') &&
+ zoomTo.isBetween(earliest, latest, null, '[]')
+ ) {
+ return [zoomFrom.toDate(), zoomTo.toDate()];
+ }
+ }
+
+ return undefined;
+ }
+
+ function calculateDefaultFocusRange(
+ autoZoomDuration: any,
+ contextAggregationInterval: any,
+ contextChartData: any,
+ contextForecastData: any
+ ) {
+ const isForecastData = contextForecastData !== undefined && contextForecastData.length > 0;
+
+ const combinedData =
+ isForecastData === false ? contextChartData : contextChartData.concat(contextForecastData);
+ const earliestDataDate = combinedData[0].date;
+ const latestDataDate = combinedData[combinedData.length - 1].date;
+
+ let rangeEarliestMs;
+ let rangeLatestMs;
+
+ if (isForecastData === true) {
+ // Return a range centred on the start of the forecast range, depending
+ // on the time range of the forecast and data.
+ const earliestForecastDataDate = contextForecastData[0].date;
+ const latestForecastDataDate = contextForecastData[contextForecastData.length - 1].date;
+
+ rangeLatestMs = Math.min(
+ earliestForecastDataDate.getTime() + autoZoomDuration / 2,
+ latestForecastDataDate.getTime()
+ );
+ rangeEarliestMs = Math.max(rangeLatestMs - autoZoomDuration, earliestDataDate.getTime());
+ } else {
+ // Returns the range that shows the most recent data at bucket span granularity.
+ rangeLatestMs = latestDataDate.getTime() + contextAggregationInterval.asMilliseconds();
+ rangeEarliestMs = Math.max(earliestDataDate.getTime(), rangeLatestMs - autoZoomDuration);
+ }
+
+ return [new Date(rangeEarliestMs), new Date(rangeLatestMs)];
+ }
+
+ // Return dataset in format used by the swimlane.
+ // i.e. array of Objects with keys date (JavaScript date) and score.
+ function processRecordScoreResults(scoreData: any) {
+ const bucketScoreData: any = [];
+ each(scoreData, (dataForTime, time) => {
+ bucketScoreData.push({
+ date: new Date(+time),
+ score: dataForTime.score,
+ });
+ });
+
+ return bucketScoreData;
+ }
+
+ // Return dataset in format used by the single metric chart.
+ // i.e. array of Objects with keys date (JavaScript date) and value,
+ // plus lower and upper keys if model plot is enabled for the series.
+ function processMetricPlotResults(metricPlotData: any, modelPlotEnabled: any) {
+ const metricPlotChartData: any = [];
+ if (modelPlotEnabled === true) {
+ each(metricPlotData, (dataForTime, time) => {
+ metricPlotChartData.push({
+ date: new Date(+time),
+ lower: dataForTime.modelLower,
+ value: dataForTime.actual,
+ upper: dataForTime.modelUpper,
+ });
+ });
+ } else {
+ each(metricPlotData, (dataForTime, time) => {
+ metricPlotChartData.push({
+ date: new Date(+time),
+ value: dataForTime.actual,
+ });
+ });
+ }
+
+ return metricPlotChartData;
+ }
+
+ // Returns forecast dataset in format used by the single metric chart.
+ // i.e. array of Objects with keys date (JavaScript date), isForecast,
+ // value, lower and upper keys.
+ function processForecastResults(forecastData: any) {
+ const forecastPlotChartData: any = [];
+ each(forecastData, (dataForTime, time) => {
+ forecastPlotChartData.push({
+ date: new Date(+time),
+ isForecast: true,
+ lower: dataForTime.forecastLower,
+ value: dataForTime.prediction,
+ upper: dataForTime.forecastUpper,
+ });
+ });
+
+ return forecastPlotChartData;
+ }
+
+ // Finds the chart point which corresponds to an anomaly with the
+ // specified time.
+ function findChartPointForAnomalyTime(
+ chartData: any,
+ anomalyTime: any,
+ aggregationInterval: any
+ ) {
+ let chartPoint;
+ if (chartData === undefined) {
+ return chartPoint;
+ }
+
+ for (let i = 0; i < chartData.length; i++) {
+ if (chartData[i].date.getTime() === anomalyTime) {
+ chartPoint = chartData[i];
+ break;
+ }
+ }
+
+ if (chartPoint === undefined) {
+ // Find the time of the point which falls immediately before the
+ // time of the anomaly. This is the start of the chart 'bucket'
+ // which contains the anomalous bucket.
+ let foundItem;
+ const intervalMs = aggregationInterval.asMilliseconds();
+ for (let i = 0; i < chartData.length; i++) {
+ const itemTime = chartData[i].date.getTime();
+ if (anomalyTime - itemTime < intervalMs) {
+ foundItem = chartData[i];
+ break;
+ }
+ }
+
+ chartPoint = foundItem;
+ }
+
+ return chartPoint;
+ }
+
+ // Uses data from the list of anomaly records to add anomalyScore,
+ // function, actual and typical properties, plus causes and multi-bucket
+ // info if applicable, to the chartData entries for anomalous buckets.
+ function processDataForFocusAnomalies(
+ chartData: ChartDataPoint[],
+ anomalyRecords: MlAnomalyRecordDoc[],
+ aggregationInterval: Interval,
+ modelPlotEnabled: boolean,
+ functionDescription?: string
+ ) {
+ const timesToAddPointsFor: number[] = [];
+
+ // Iterate through the anomaly records making sure we have chart points for each anomaly.
+ const intervalMs = aggregationInterval.asMilliseconds();
+ let lastChartDataPointTime: any;
+ if (chartData !== undefined && chartData.length > 0) {
+ lastChartDataPointTime = chartData[chartData.length - 1].date.getTime();
+ }
+ anomalyRecords.forEach((record: MlAnomalyRecordDoc) => {
+ const recordTime = record[TIME_FIELD_NAME];
+ const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval);
+ if (chartPoint === undefined) {
+ const timeToAdd = Math.floor(recordTime / intervalMs) * intervalMs;
+ if (timesToAddPointsFor.indexOf(timeToAdd) === -1 && timeToAdd !== lastChartDataPointTime) {
+ timesToAddPointsFor.push(timeToAdd);
+ }
+ }
+ });
+
+ timesToAddPointsFor.sort((a, b) => a - b);
+
+ timesToAddPointsFor.forEach((time) => {
+ const pointToAdd: ChartDataPoint = {
+ date: new Date(time),
+ value: null,
+ };
+
+ if (modelPlotEnabled === true) {
+ pointToAdd.upper = null;
+ pointToAdd.lower = null;
+ }
+ chartData.push(pointToAdd);
+ });
+
+ // Iterate through the anomaly records adding the
+ // various properties required for display.
+ anomalyRecords.forEach((record) => {
+ // Look for a chart point with the same time as the record.
+ // If none found, find closest time in chartData set.
+ const recordTime = record[TIME_FIELD_NAME];
+ if (
+ record.function === ML_JOB_AGGREGATION.METRIC &&
+ record.function_description !== functionDescription
+ )
+ return;
+
+ const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval);
+ if (chartPoint !== undefined) {
+ // If chart aggregation interval > bucket span, there may be more than
+ // one anomaly record in the interval, so use the properties from
+ // the record with the highest anomalyScore.
+ const recordScore = record.record_score;
+ const pointScore = chartPoint.anomalyScore;
+ if (pointScore === undefined || pointScore < recordScore) {
+ chartPoint.anomalyScore = recordScore;
+ chartPoint.function = record.function;
+
+ if (record.actual !== undefined) {
+ // If cannot match chart point for anomaly time
+ // substitute the value with the record's actual so it won't plot as null/0
+ if (chartPoint.value === null || record.function === ML_JOB_AGGREGATION.METRIC) {
+ chartPoint.value = Array.isArray(record.actual) ? record.actual[0] : record.actual;
+ }
+
+ chartPoint.actual = record.actual;
+ chartPoint.typical = record.typical;
+ } else {
+ const causes = get(record, 'causes', []);
+ if (causes.length > 0) {
+ chartPoint.byFieldName = record.by_field_name;
+ chartPoint.numberOfCauses = causes.length;
+ if (causes.length === 1) {
+ // If only a single cause, copy actual and typical values to the top level.
+ const cause = record.causes![0];
+ chartPoint.actual = cause.actual;
+ chartPoint.typical = cause.typical;
+ // substitute the value with the record's actual so it won't plot as null/0
+ if (chartPoint.value === null) {
+ chartPoint.value = cause.actual;
+ }
+ }
+ }
+ }
+
+ if (
+ record.anomaly_score_explanation !== undefined &&
+ record.anomaly_score_explanation.multi_bucket_impact !== undefined
+ ) {
+ chartPoint.multiBucketImpact = record.anomaly_score_explanation.multi_bucket_impact;
+ }
+
+ chartPoint.isMultiBucketAnomaly = isMultiBucketAnomaly(record);
+ }
+ }
+ });
+
+ return chartData;
+ }
+
+ function findChartPointForScheduledEvent(chartData: any, eventTime: any) {
+ let chartPoint;
+ if (chartData === undefined) {
+ return chartPoint;
+ }
+
+ for (let i = 0; i < chartData.length; i++) {
+ if (chartData[i].date.getTime() === eventTime) {
+ chartPoint = chartData[i];
+ break;
+ }
+ }
+
+ return chartPoint;
+ }
+ // Adds a scheduledEvents property to any points in the chart data set
+ // which correspond to times of scheduled events for the job.
+ function processScheduledEventsForChart(
+ chartData: ChartDataPoint[],
+ scheduledEvents: Array<{ events: any; time: number }> | undefined,
+ aggregationInterval: TimeBucketsInterval
+ ) {
+ if (scheduledEvents !== undefined) {
+ const timesToAddPointsFor: number[] = [];
+
+ // Iterate through the scheduled events making sure we have a chart point for each event.
+ const intervalMs = aggregationInterval.asMilliseconds();
+ let lastChartDataPointTime: number | undefined;
+ if (chartData !== undefined && chartData.length > 0) {
+ lastChartDataPointTime = chartData[chartData.length - 1].date.getTime();
+ }
+
+ // In case there's no chart data/sparse data during these scheduled events
+ // ensure we add chart points at every aggregation interval for these scheduled events.
+ let sortRequired = false;
+ each(scheduledEvents, (events, time) => {
+ const exactChartPoint = findChartPointForScheduledEvent(chartData, +time);
+
+ if (exactChartPoint !== undefined) {
+ exactChartPoint.scheduledEvents = events;
+ } else {
+ const timeToAdd: number = Math.floor(time / intervalMs) * intervalMs;
+ if (
+ timesToAddPointsFor.indexOf(timeToAdd) === -1 &&
+ timeToAdd !== lastChartDataPointTime
+ ) {
+ const pointToAdd = {
+ date: new Date(timeToAdd),
+ value: null,
+ scheduledEvents: events,
+ };
+
+ chartData.push(pointToAdd);
+ sortRequired = true;
+ }
+ }
+ });
+
+ // Sort chart data by time if extra points were added at the end of the array for scheduled events.
+ if (sortRequired) {
+ chartData.sort((a, b) => a.date.getTime() - b.date.getTime());
+ }
+ }
+
+ return chartData;
+ }
+
+ function getFocusData(
+ criteriaFields: CriteriaField[],
+ detectorIndex: number,
+ focusAggregationInterval: TimeBucketsInterval,
+ forecastId: string,
+ modelPlotEnabled: boolean,
+ nonBlankEntities: any[],
+ searchBounds: any,
+ selectedJob: Job,
+ functionDescription?: string | undefined
+ ): Observable {
+ const esFunctionToPlotIfMetric =
+ functionDescription !== undefined
+ ? aggregationTypeTransform.toES(functionDescription)
+ : functionDescription;
+
+ return forkJoin([
+ // Query 1 - load metric data across selected time range.
+ mlTimeSeriesSearchService.getMetricData(
+ selectedJob,
+ detectorIndex,
+ nonBlankEntities,
+ searchBounds.min.valueOf(),
+ searchBounds.max.valueOf(),
+ focusAggregationInterval.asMilliseconds(),
+ esFunctionToPlotIfMetric
+ ),
+ // Query 2 - load all the records across selected time range for the chart anomaly markers.
+ mlApiServices.results.getAnomalyRecords$(
+ [selectedJob.job_id],
+ criteriaFields,
+ 0,
+ searchBounds.min.valueOf(),
+ searchBounds.max.valueOf(),
+ focusAggregationInterval.expression,
+ functionDescription
+ ),
+ // Query 3 - load any scheduled events for the selected job.
+ mlResultsService.getScheduledEventsByBucket(
+ [selectedJob.job_id],
+ searchBounds.min.valueOf(),
+ searchBounds.max.valueOf(),
+ focusAggregationInterval.asMilliseconds(),
+ 1,
+ MAX_SCHEDULED_EVENTS
+ ),
+ // Query 4 - load any annotations for the selected job.
+ mlApiServices.annotations
+ .getAnnotations$({
+ jobIds: [selectedJob.job_id],
+ earliestMs: searchBounds.min.valueOf(),
+ latestMs: searchBounds.max.valueOf(),
+ maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE,
+ detectorIndex,
+ entities: nonBlankEntities,
+ })
+ .pipe(
+ catchError((resp) =>
+ of({
+ annotations: {},
+ totalCount: 0,
+ error: extractErrorMessage(resp),
+ success: false,
+ } as GetAnnotationsResponse)
+ )
+ ),
+ // Plus query for forecast data if there is a forecastId stored in the appState.
+ forecastId !== undefined
+ ? (() => {
+ let aggType;
+ const detector = selectedJob.analysis_config.detectors[detectorIndex];
+ const esAgg = mlFunctionToESAggregation(detector.function);
+ if (!modelPlotEnabled && (esAgg === 'sum' || esAgg === 'count')) {
+ aggType = { avg: 'sum', max: 'sum', min: 'sum' };
+ }
+ return mlForecastService.getForecastData(
+ selectedJob,
+ detectorIndex,
+ forecastId,
+ nonBlankEntities,
+ searchBounds.min.valueOf(),
+ searchBounds.max.valueOf(),
+ focusAggregationInterval.asMilliseconds(),
+ aggType
+ );
+ })()
+ : of(null),
+ ]).pipe(
+ map(
+ ([metricData, recordsForCriteria, scheduledEventsByBucket, annotations, forecastData]) => {
+ // Sort in descending time order before storing in scope.
+ const anomalyRecords = recordsForCriteria?.records
+ .sort((a, b) => a[TIME_FIELD_NAME] - b[TIME_FIELD_NAME])
+ .reverse();
+
+ const scheduledEvents = scheduledEventsByBucket?.events[selectedJob.job_id];
+
+ let focusChartData = processMetricPlotResults(metricData.results, modelPlotEnabled);
+ // Tell the results container directives to render the focus chart.
+ focusChartData = processDataForFocusAnomalies(
+ focusChartData,
+ anomalyRecords,
+ focusAggregationInterval,
+ modelPlotEnabled,
+ functionDescription
+ );
+ focusChartData = processScheduledEventsForChart(
+ focusChartData,
+ scheduledEvents,
+ focusAggregationInterval
+ );
+
+ const refreshFocusData: FocusData = {
+ scheduledEvents,
+ anomalyRecords,
+ focusChartData,
+ };
+
+ if (annotations) {
+ if (annotations.error !== undefined) {
+ refreshFocusData.focusAnnotationError = annotations.error;
+ refreshFocusData.focusAnnotationData = [];
+ } else {
+ refreshFocusData.focusAnnotationData = (
+ annotations.annotations[selectedJob.job_id] ?? []
+ )
+ .sort((a, b) => {
+ return a.timestamp - b.timestamp;
+ })
+ .map((d, i: number) => {
+ d.key = (i + 1).toString();
+ return d;
+ });
+ }
+ }
+
+ if (forecastData) {
+ refreshFocusData.focusForecastData = processForecastResults(forecastData.results);
+ refreshFocusData.showForecastCheckbox = refreshFocusData.focusForecastData.length > 0;
+ }
+ return refreshFocusData;
+ }
+ )
+ );
+ }
+
+ return {
+ getAutoZoomDuration,
+ calculateAggregationInterval,
+ calculateInitialFocusRange,
+ calculateDefaultFocusRange,
+ processRecordScoreResults,
+ processMetricPlotResults,
+ processForecastResults,
+ findChartPointForAnomalyTime,
+ processDataForFocusAnomalies,
+ findChartPointForScheduledEvent,
+ processScheduledEventsForChart,
+ getFocusData,
+ };
+}
+
+export function useTimeSeriesExplorerService(): TimeSeriesExplorerService {
+ const {
+ services: {
+ uiSettings,
+ mlServices: { mlApiServices },
+ },
+ } = useMlKibana();
+ const mlResultsService = mlResultsServiceProvider(mlApiServices);
+
+ const mlTimeSeriesExplorer = useMemo(
+ () => timeSeriesExplorerServiceFactory(uiSettings, mlApiServices, mlResultsService),
+ [uiSettings, mlApiServices, mlResultsService]
+ );
+ return mlTimeSeriesExplorer;
+}
+
+export type TimeSeriesExplorerService = ReturnType;
diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx
index 00c4a02d4e929..182d070266c9a 100644
--- a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx
+++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx
@@ -26,7 +26,8 @@ import { JobSelectorFlyout } from './components/job_selector_flyout';
*/
export async function resolveJobSelection(
coreStart: CoreStart,
- selectedJobIds?: JobId[]
+ selectedJobIds?: JobId[],
+ singleSelection: boolean = false
): Promise<{ jobIds: string[]; groups: Array<{ groupId: string; jobIds: string[] }> }> {
const {
http,
@@ -74,7 +75,7 @@ export async function resolveJobSelection(
selectedIds={selectedJobIds}
withTimeRangeSelector={false}
dateFormatTz={dateFormatTz}
- singleSelection={false}
+ singleSelection={singleSelection}
timeseriesOnly={true}
onFlyoutClose={onFlyoutClose}
onSelectionConfirmed={onSelectionConfirmed}
diff --git a/x-pack/plugins/ml/public/embeddables/constants.ts b/x-pack/plugins/ml/public/embeddables/constants.ts
index cfe50f25cd889..1001cd89c7498 100644
--- a/x-pack/plugins/ml/public/embeddables/constants.ts
+++ b/x-pack/plugins/ml/public/embeddables/constants.ts
@@ -7,6 +7,7 @@
export const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane' as const;
export const ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE = 'ml_anomaly_charts' as const;
+export const ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE = 'ml_single_metric_viewer' as const;
export type AnomalySwimLaneEmbeddableType = typeof ANOMALY_SWIMLANE_EMBEDDABLE_TYPE;
export type AnomalyExplorerChartsEmbeddableType = typeof ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE;
diff --git a/x-pack/plugins/ml/public/embeddables/index.ts b/x-pack/plugins/ml/public/embeddables/index.ts
index 0a505fe04ea85..9f0d2d75b1162 100644
--- a/x-pack/plugins/ml/public/embeddables/index.ts
+++ b/x-pack/plugins/ml/public/embeddables/index.ts
@@ -9,6 +9,7 @@ import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
import { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane';
import type { MlCoreSetup } from '../plugin';
import { AnomalyChartsEmbeddableFactory } from './anomaly_charts';
+import { SingleMetricViewerEmbeddableFactory } from './single_metric_viewer';
export * from './constants';
export * from './types';
@@ -25,6 +26,8 @@ export function registerEmbeddables(embeddable: EmbeddableSetup, core: MlCoreSet
);
const anomalyChartsFactory = new AnomalyChartsEmbeddableFactory(core.getStartServices);
-
embeddable.registerEmbeddableFactory(anomalyChartsFactory.type, anomalyChartsFactory);
+
+ const singleMetricViewerFactory = new SingleMetricViewerEmbeddableFactory(core.getStartServices);
+ embeddable.registerEmbeddableFactory(singleMetricViewerFactory.type, singleMetricViewerFactory);
}
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/_index.scss b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/_index.scss
new file mode 100644
index 0000000000000..b6f91cc749dcc
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/_index.scss
@@ -0,0 +1,6 @@
+// ML has it's own variables for coloring
+@import '../../application/variables';
+
+// Protect the rest of Kibana from ML generic namespacing
+@import '../../application/timeseriesexplorer/timeseriesexplorer';
+@import '../../application/timeseriesexplorer/timeseriesexplorer_annotations';
\ No newline at end of file
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx
new file mode 100644
index 0000000000000..88c120c9747e1
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx
@@ -0,0 +1,204 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
+import { EuiResizeObserver } from '@elastic/eui';
+import { Observable } from 'rxjs';
+import { throttle } from 'lodash';
+import { MlJob } from '@elastic/elasticsearch/lib/api/types';
+import usePrevious from 'react-use/lib/usePrevious';
+import { useToastNotificationService } from '../../application/services/toast_notification_service';
+import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context';
+import { useSingleMetricViewerInputResolver } from './use_single_metric_viewer_input_resolver';
+import type { ISingleMetricViewerEmbeddable } from './single_metric_viewer_embeddable';
+import type {
+ SingleMetricViewerEmbeddableInput,
+ AnomalyChartsEmbeddableOutput,
+ SingleMetricViewerEmbeddableServices,
+} from '..';
+import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '..';
+import { TimeSeriesExplorerEmbeddableChart } from '../../application/timeseriesexplorer/timeseriesexplorer_embeddable_chart';
+import { APP_STATE_ACTION } from '../../application/timeseriesexplorer/timeseriesexplorer_constants';
+import { useTimeSeriesExplorerService } from '../../application/util/time_series_explorer_service';
+import './_index.scss';
+
+const RESIZE_THROTTLE_TIME_MS = 500;
+
+interface AppStateZoom {
+ from?: string;
+ to?: string;
+}
+
+export interface EmbeddableSingleMetricViewerContainerProps {
+ id: string;
+ embeddableContext: InstanceType;
+ embeddableInput: Observable;
+ services: SingleMetricViewerEmbeddableServices;
+ refresh: Observable;
+ onInputChange: (input: Partial) => void;
+ onOutputChange: (output: Partial) => void;
+ onRenderComplete: () => void;
+ onLoading: () => void;
+ onError: (error: Error) => void;
+}
+
+export const EmbeddableSingleMetricViewerContainer: FC<
+ EmbeddableSingleMetricViewerContainerProps
+> = ({
+ id,
+ embeddableContext,
+ embeddableInput,
+ services,
+ refresh,
+ onInputChange,
+ onOutputChange,
+ onRenderComplete,
+ onError,
+ onLoading,
+}) => {
+ useEmbeddableExecutionContext(
+ services[0].executionContext,
+ embeddableInput,
+ ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE,
+ id
+ );
+ const [chartWidth, setChartWidth] = useState(0);
+ const [zoom, setZoom] = useState();
+ const [selectedForecastId, setSelectedForecastId] = useState();
+ const [detectorIndex, setDetectorIndex] = useState(0);
+ const [selectedJob, setSelectedJob] = useState();
+ const [autoZoomDuration, setAutoZoomDuration] = useState();
+
+ const { mlApiServices } = services[2];
+ const { data, bounds, lastRefresh } = useSingleMetricViewerInputResolver(
+ embeddableInput,
+ refresh,
+ services[1].data.query.timefilter.timefilter,
+ onRenderComplete
+ );
+ const selectedJobId = data?.jobIds[0];
+ const previousRefresh = usePrevious(lastRefresh ?? 0);
+ const mlTimeSeriesExplorer = useTimeSeriesExplorerService();
+
+ // Holds the container height for previously fetched data
+ const containerHeightRef = useRef();
+ const toastNotificationService = useToastNotificationService();
+
+ useEffect(
+ function setUpSelectedJob() {
+ async function fetchSelectedJob() {
+ if (mlApiServices && selectedJobId !== undefined) {
+ const { jobs } = await mlApiServices.getJobs({ jobId: selectedJobId });
+ const job = jobs[0];
+ setSelectedJob(job);
+ }
+ }
+ fetchSelectedJob();
+ },
+ [selectedJobId, mlApiServices]
+ );
+
+ useEffect(
+ function setUpAutoZoom() {
+ let zoomDuration: number | undefined;
+ if (selectedJobId !== undefined && selectedJob !== undefined) {
+ zoomDuration = mlTimeSeriesExplorer.getAutoZoomDuration(selectedJob);
+ setAutoZoomDuration(zoomDuration);
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [selectedJobId, selectedJob?.job_id, mlTimeSeriesExplorer]
+ );
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const resizeHandler = useCallback(
+ throttle((e: { width: number; height: number }) => {
+ // Keep previous container height so it doesn't change the page layout
+ containerHeightRef.current = e.height;
+
+ if (Math.abs(chartWidth - e.width) > 20) {
+ setChartWidth(e.width);
+ }
+ }, RESIZE_THROTTLE_TIME_MS),
+ [chartWidth]
+ );
+
+ const appStateHandler = useCallback(
+ (action: string, payload?: any) => {
+ /**
+ * Empty zoom indicates that chart hasn't been rendered yet,
+ * hence any updates prior that should replace the URL state.
+ */
+
+ switch (action) {
+ case APP_STATE_ACTION.SET_DETECTOR_INDEX:
+ setDetectorIndex(payload);
+ break;
+
+ case APP_STATE_ACTION.SET_FORECAST_ID:
+ setSelectedForecastId(payload);
+ setZoom(undefined);
+ break;
+
+ case APP_STATE_ACTION.SET_ZOOM:
+ setZoom(payload);
+ break;
+
+ case APP_STATE_ACTION.UNSET_ZOOM:
+ setZoom(undefined);
+ break;
+ }
+ },
+
+ [setZoom, setDetectorIndex, setSelectedForecastId]
+ );
+
+ const containerPadding = 10;
+
+ return (
+
+ {(resizeRef) => (
+
+ {data !== undefined && autoZoomDuration !== undefined && (
+
+ )}
+
+ )}
+
+ );
+};
+
+// required for dynamic import using React.lazy()
+// eslint-disable-next-line import/no-default-export
+export default EmbeddableSingleMetricViewerContainer;
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container_lazy.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container_lazy.tsx
new file mode 100644
index 0000000000000..0a69aaf2c2deb
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container_lazy.tsx
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+export const EmbeddableSingleMetricViewerContainer = React.lazy(
+ () => import('./embeddable_single_metric_viewer_container')
+);
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/index.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/index.ts
new file mode 100644
index 0000000000000..9afdbe3d1298c
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { SingleMetricViewerEmbeddableFactory } from './single_metric_viewer_embeddable_factory';
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx
new file mode 100644
index 0000000000000..82a1b5abc8b63
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx
@@ -0,0 +1,136 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { Suspense } from 'react';
+import ReactDOM from 'react-dom';
+import { pick } from 'lodash';
+
+import { Embeddable } from '@kbn/embeddable-plugin/public';
+
+import { CoreStart } from '@kbn/core/public';
+import { i18n } from '@kbn/i18n';
+import { Subject } from 'rxjs';
+import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
+import { IContainer } from '@kbn/embeddable-plugin/public';
+import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
+import { UI_SETTINGS } from '@kbn/data-plugin/common';
+import { EmbeddableSingleMetricViewerContainer } from './embeddable_single_metric_viewer_container_lazy';
+import type { JobId } from '../../../common/types/anomaly_detection_jobs';
+import type { MlDependencies } from '../../application/app';
+import {
+ ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE,
+ SingleMetricViewerEmbeddableInput,
+ AnomalyChartsEmbeddableOutput,
+ SingleMetricViewerServices,
+} from '..';
+import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback';
+
+export const getDefaultSingleMetricViewerPanelTitle = (jobIds: JobId[]) =>
+ i18n.translate('xpack.ml.singleMetricViewerEmbeddable.title', {
+ defaultMessage: 'ML single metric viewer chart for {jobIds}',
+ values: { jobIds: jobIds.join(', ') },
+ });
+
+export type ISingleMetricViewerEmbeddable = typeof SingleMetricViewerEmbeddable;
+
+export class SingleMetricViewerEmbeddable extends Embeddable<
+ SingleMetricViewerEmbeddableInput,
+ AnomalyChartsEmbeddableOutput
+> {
+ private node?: HTMLElement;
+ private reload$ = new Subject();
+ public readonly type: string = ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE;
+
+ constructor(
+ initialInput: SingleMetricViewerEmbeddableInput,
+ public services: [CoreStart, MlDependencies, SingleMetricViewerServices],
+ parent?: IContainer
+ ) {
+ super(initialInput, {} as AnomalyChartsEmbeddableOutput, parent);
+ }
+
+ public onLoading() {
+ this.renderComplete.dispatchInProgress();
+ this.updateOutput({ loading: true, error: undefined });
+ }
+
+ public onError(error: Error) {
+ this.renderComplete.dispatchError();
+ this.updateOutput({ loading: false, error: { name: error.name, message: error.message } });
+ }
+
+ public onRenderComplete() {
+ this.renderComplete.dispatchComplete();
+ this.updateOutput({ loading: false, error: undefined });
+ }
+
+ public render(node: HTMLElement) {
+ super.render(node);
+ this.node = node;
+
+ // required for the export feature to work
+ this.node.setAttribute('data-shared-item', '');
+
+ const I18nContext = this.services[0].i18n.Context;
+ const theme$ = this.services[0].theme.theme$;
+
+ const datePickerDeps: DatePickerDependencies = {
+ ...pick(this.services[0], ['http', 'notifications', 'theme', 'uiSettings', 'i18n']),
+ data: this.services[1].data,
+ uiSettingsKeys: UI_SETTINGS,
+ showFrozenDataTierChoice: false,
+ };
+
+ ReactDOM.render(
+
+
+
+
+ }>
+
+
+
+
+
+ ,
+ node
+ );
+ }
+
+ public destroy() {
+ super.destroy();
+ if (this.node) {
+ ReactDOM.unmountComponentAtNode(this.node);
+ }
+ }
+
+ public reload() {
+ this.reload$.next();
+ }
+
+ public supportedTriggers() {
+ return [];
+ }
+}
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts
new file mode 100644
index 0000000000000..06b2f9b024bfa
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts
@@ -0,0 +1,131 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import type { StartServicesAccessor } from '@kbn/core/public';
+import type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
+
+import { PLUGIN_ICON, PLUGIN_ID, ML_APP_NAME } from '../../../common/constants/app';
+import {
+ ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE,
+ SingleMetricViewerEmbeddableInput,
+ SingleMetricViewerEmbeddableServices,
+} from '..';
+import type { MlPluginStart, MlStartDependencies } from '../../plugin';
+import type { MlDependencies } from '../../application/app';
+import { HttpService } from '../../application/services/http_service';
+import { AnomalyExplorerChartsService } from '../../application/services/anomaly_explorer_charts_service';
+
+export class SingleMetricViewerEmbeddableFactory
+ implements EmbeddableFactoryDefinition
+{
+ public readonly type = ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE;
+
+ public readonly grouping = [
+ {
+ id: PLUGIN_ID,
+ getDisplayName: () => ML_APP_NAME,
+ getIconType: () => PLUGIN_ICON,
+ },
+ ];
+
+ constructor(
+ private getStartServices: StartServicesAccessor
+ ) {}
+
+ public async isEditable() {
+ return true;
+ }
+
+ public getDisplayName() {
+ return i18n.translate('xpack.ml.components.mlSingleMetricViewerEmbeddable.displayName', {
+ defaultMessage: 'Single metric viewer',
+ });
+ }
+
+ public getDescription() {
+ return i18n.translate('xpack.ml.components.mlSingleMetricViewerEmbeddable.description', {
+ defaultMessage: 'View anomaly detection single metric results in a chart.',
+ });
+ }
+
+ public async getExplicitInput(): Promise> {
+ const [coreStart, pluginStart, singleMetricServices] = await this.getServices();
+
+ try {
+ const { resolveEmbeddableSingleMetricViewerUserInput } = await import(
+ './single_metric_viewer_setup_flyout'
+ );
+ return await resolveEmbeddableSingleMetricViewerUserInput(
+ coreStart,
+ pluginStart,
+ singleMetricServices
+ );
+ } catch (e) {
+ return Promise.reject();
+ }
+ }
+
+ private async getServices(): Promise {
+ const [
+ [coreStart, pluginsStart],
+ { AnomalyDetectorService },
+ { fieldFormatServiceFactory },
+ { indexServiceFactory },
+ { mlApiServicesProvider },
+ { mlResultsServiceProvider },
+ { timeSeriesSearchServiceFactory },
+ ] = await Promise.all([
+ await this.getStartServices(),
+ await import('../../application/services/anomaly_detector_service'),
+ await import('../../application/services/field_format_service_factory'),
+ await import('../../application/util/index_service'),
+ await import('../../application/services/ml_api_service'),
+ await import('../../application/services/results_service'),
+ await import(
+ '../../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'
+ ),
+ ]);
+
+ const httpService = new HttpService(coreStart.http);
+ const anomalyDetectorService = new AnomalyDetectorService(httpService);
+ const mlApiServices = mlApiServicesProvider(httpService);
+ const mlResultsService = mlResultsServiceProvider(mlApiServices);
+ const mlIndexUtils = indexServiceFactory(pluginsStart.data.dataViews);
+ const mlTimeSeriesSearchService = timeSeriesSearchServiceFactory(
+ mlResultsService,
+ mlApiServices
+ );
+ const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils);
+
+ const anomalyExplorerService = new AnomalyExplorerChartsService(
+ pluginsStart.data.query.timefilter.timefilter,
+ mlApiServices,
+ mlResultsService
+ );
+
+ return [
+ coreStart,
+ pluginsStart as MlDependencies,
+ {
+ anomalyDetectorService,
+ anomalyExplorerService,
+ mlResultsService,
+ mlApiServices,
+ mlTimeSeriesSearchService,
+ mlFieldFormatService,
+ },
+ ];
+ }
+
+ public async create(initialInput: SingleMetricViewerEmbeddableInput, parent?: IContainer) {
+ const services = await this.getServices();
+ const { SingleMetricViewerEmbeddable } = await import('./single_metric_viewer_embeddable');
+ return new SingleMetricViewerEmbeddable(initialInput, services, parent);
+ }
+}
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx
new file mode 100644
index 0000000000000..89af056068063
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useState } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiForm,
+ EuiFormRow,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiFieldText,
+ EuiModal,
+ EuiSpacer,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { MlJob } from '@elastic/elasticsearch/lib/api/types';
+import type { SingleMetricViewerServices } from '..';
+import { TimeRangeBounds } from '../../application/util/time_buckets';
+import { SeriesControls } from '../../application/timeseriesexplorer/components/series_controls';
+import {
+ APP_STATE_ACTION,
+ type TimeseriesexplorerActionType,
+} from '../../application/timeseriesexplorer/timeseriesexplorer_constants';
+
+export interface SingleMetricViewerInitializerProps {
+ bounds: TimeRangeBounds;
+ defaultTitle: string;
+ initialInput?: SingleMetricViewerServices;
+ job: MlJob;
+ onCreate: (props: {
+ panelTitle: string;
+ functionDescription?: string;
+ selectedDetectorIndex: number;
+ selectedEntities: any;
+ }) => void;
+ onCancel: () => void;
+}
+
+export const SingleMetricViewerInitializer: FC = ({
+ bounds,
+ defaultTitle,
+ initialInput,
+ job,
+ onCreate,
+ onCancel,
+}) => {
+ const [panelTitle, setPanelTitle] = useState(defaultTitle);
+ const [functionDescription, setFunctionDescription] = useState();
+ const [selectedDetectorIndex, setSelectedDetectorIndex] = useState(0);
+ const [selectedEntities, setSelectedEntities] = useState();
+
+ const isPanelTitleValid = panelTitle.length > 0;
+
+ const handleStateUpdate = (action: TimeseriesexplorerActionType, payload: any) => {
+ switch (action) {
+ case APP_STATE_ACTION.SET_ENTITIES:
+ setSelectedEntities(payload);
+ break;
+ case APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION:
+ setFunctionDescription(payload);
+ break;
+ case APP_STATE_ACTION.SET_DETECTOR_INDEX:
+ setSelectedDetectorIndex(payload);
+ break;
+ default:
+ break;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ }
+ isInvalid={!isPanelTitleValid}
+ >
+ setPanelTitle(e.target.value)}
+ isInvalid={!isPanelTitleValid}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx
new file mode 100644
index 0000000000000..e9822c01f865a
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type { CoreStart } from '@kbn/core/public';
+import { toMountPoint } from '@kbn/react-kibana-mount';
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { getDefaultSingleMetricViewerPanelTitle } from './single_metric_viewer_embeddable';
+import type { SingleMetricViewerEmbeddableInput, SingleMetricViewerServices } from '..';
+import { resolveJobSelection } from '../common/resolve_job_selection';
+import { SingleMetricViewerInitializer } from './single_metric_viewer_initializer';
+import type { MlStartDependencies } from '../../plugin';
+
+export async function resolveEmbeddableSingleMetricViewerUserInput(
+ coreStart: CoreStart,
+ pluginStart: MlStartDependencies,
+ input: SingleMetricViewerServices
+): Promise> {
+ const { overlays, theme, i18n } = coreStart;
+ const { mlApiServices } = input;
+ const timefilter = pluginStart.data.query.timefilter.timefilter;
+
+ return new Promise(async (resolve, reject) => {
+ try {
+ const { jobIds } = await resolveJobSelection(coreStart, undefined, true);
+ const title = getDefaultSingleMetricViewerPanelTitle(jobIds);
+ const { jobs } = await mlApiServices.getJobs({ jobId: jobIds.join(',') });
+
+ const modalSession = overlays.openModal(
+ toMountPoint(
+
+ {
+ modalSession.close();
+ resolve({
+ jobIds,
+ title: panelTitle,
+ functionDescription,
+ panelTitle,
+ selectedDetectorIndex,
+ selectedEntities,
+ });
+ }}
+ onCancel={() => {
+ modalSession.close();
+ reject();
+ }}
+ />
+ ,
+ { theme, i18n }
+ )
+ );
+ } catch (error) {
+ reject(error);
+ }
+ });
+}
diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/use_single_metric_viewer_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/use_single_metric_viewer_input_resolver.ts
new file mode 100644
index 0000000000000..c9f9d57fd7803
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/use_single_metric_viewer_input_resolver.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { useEffect, useState } from 'react';
+import { combineLatest, Observable } from 'rxjs';
+import { startWith } from 'rxjs/operators';
+import { TimefilterContract } from '@kbn/data-plugin/public';
+import { SingleMetricViewerEmbeddableInput } from '..';
+import type { TimeRangeBounds } from '../../application/util/time_buckets';
+
+export function useSingleMetricViewerInputResolver(
+ embeddableInput: Observable,
+ refresh: Observable,
+ timefilter: TimefilterContract,
+ onRenderComplete: () => void
+) {
+ const [data, setData] = useState();
+ const [bounds, setBounds] = useState();
+ const [lastRefresh, setLastRefresh] = useState();
+
+ useEffect(function subscribeToEmbeddableInput() {
+ const subscription = combineLatest([embeddableInput, refresh.pipe(startWith(null))]).subscribe(
+ (input) => {
+ if (input !== undefined) {
+ setData(input[0]);
+ if (timefilter !== undefined) {
+ const { timeRange } = input[0];
+ const currentBounds = timefilter.calculateBounds(timeRange);
+ setBounds(currentBounds);
+ setLastRefresh(Date.now());
+ }
+ onRenderComplete();
+ }
+ }
+ );
+
+ return () => subscription.unsubscribe();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return { data, bounds, lastRefresh };
+}
diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts
index 48a7b5d43a5ac..56a33f488d534 100644
--- a/x-pack/plugins/ml/public/embeddables/types.ts
+++ b/x-pack/plugins/ml/public/embeddables/types.ts
@@ -27,6 +27,9 @@ import {
MlEmbeddableTypes,
} from './constants';
import { MlResultsService } from '../application/services/results_service';
+import type { MlApiServices } from '../application/services/ml_api_service';
+import type { MlFieldFormatService } from '../application/services/field_format_service';
+import type { MlTimeSeriesSeachService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service';
export interface AnomalySwimlaneEmbeddableCustomInput {
jobIds: JobId[];
@@ -100,13 +103,45 @@ export interface AnomalyChartsEmbeddableCustomInput {
export type AnomalyChartsEmbeddableInput = EmbeddableInput & AnomalyChartsEmbeddableCustomInput;
+export interface SingleMetricViewerEmbeddableCustomInput {
+ jobIds: JobId[];
+ title: string;
+ functionDescription?: string;
+ panelTitle: string;
+ selectedDetectorIndex: number;
+ selectedEntities: MlEntityField[];
+ // Embeddable inputs which are not included in the default interface
+ filters: Filter[];
+ query: Query;
+ refreshConfig: RefreshInterval;
+ timeRange: TimeRange;
+}
+
+export type SingleMetricViewerEmbeddableInput = EmbeddableInput &
+ SingleMetricViewerEmbeddableCustomInput;
+
export interface AnomalyChartsServices {
anomalyDetectorService: AnomalyDetectorService;
anomalyExplorerService: AnomalyExplorerChartsService;
mlResultsService: MlResultsService;
+ mlApiServices?: MlApiServices;
+}
+
+export interface SingleMetricViewerServices {
+ anomalyExplorerService: AnomalyExplorerChartsService;
+ anomalyDetectorService: AnomalyDetectorService;
+ mlApiServices: MlApiServices;
+ mlFieldFormatService: MlFieldFormatService;
+ mlResultsService: MlResultsService;
+ mlTimeSeriesSearchService?: MlTimeSeriesSeachService;
}
export type AnomalyChartsEmbeddableServices = [CoreStart, MlDependencies, AnomalyChartsServices];
+export type SingleMetricViewerEmbeddableServices = [
+ CoreStart,
+ MlDependencies,
+ SingleMetricViewerServices
+];
export interface AnomalyChartsCustomOutput {
entityFields?: MlEntityField[];
severity?: number;
From 05555d0bce33c146130a92b7c81f412558d9d72d Mon Sep 17 00:00:00 2001
From: Rodney Norris
Date: Thu, 8 Feb 2024 14:34:13 -0600
Subject: [PATCH 048/104] [Dev Tools] Introduce UI setting for Docked Console
(#176414)
## Summary
Introduce a new UI setting to allow users to disable the "Docked
Console" aka the EmbeddableConsole from the console plugin. This console
is currently used on Search pages and Index Management pages for 8.13,
and will be added to more pages in future releases.
### Screenshots
![image](https://github.com/elastic/kibana/assets/1972968/c8b7ed8a-8695-486d-aef5-d403ea4d4547)
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../settings/utilities/category/const.ts | 2 ++
.../utilities/category/get_category_name.ts | 4 +++
.../settings/search_project/index.ts | 6 +++-
.../settings/search_project/tsconfig.json | 1 +
src/plugins/console/public/plugin.ts | 7 +++-
src/plugins/dev_tools/common/constants.ts | 16 ++++++++++
src/plugins/dev_tools/common/index.ts | 9 ++++++
src/plugins/dev_tools/public/index.ts | 1 +
src/plugins/dev_tools/server/plugin.ts | 9 ++++--
src/plugins/dev_tools/server/ui_settings.ts | 32 +++++++++++++++++++
src/plugins/dev_tools/tsconfig.json | 2 +-
.../server/collectors/management/schema.ts | 4 +++
.../server/collectors/management/types.ts | 1 +
src/plugins/telemetry/schema/oss_plugins.json | 6 ++++
14 files changed, 95 insertions(+), 5 deletions(-)
create mode 100644 src/plugins/dev_tools/common/constants.ts
create mode 100644 src/plugins/dev_tools/common/index.ts
create mode 100644 src/plugins/dev_tools/server/ui_settings.ts
diff --git a/packages/kbn-management/settings/utilities/category/const.ts b/packages/kbn-management/settings/utilities/category/const.ts
index 67a17ecb0cc7a..9f9d65a34a04b 100644
--- a/packages/kbn-management/settings/utilities/category/const.ts
+++ b/packages/kbn-management/settings/utilities/category/const.ts
@@ -22,6 +22,7 @@ export const SECURITY_SOLUTION_CATEGORY = 'securitySolution';
export const TIMELION_CATEGORY = 'timelion';
export const VISUALIZATION_CATEGORY = 'visualization';
export const ENTERPRISE_SEARCH_CATEGORY = 'enterpriseSearch';
+export const DEV_TOOLS_CATEGORY = 'devTools';
export const CATEGORY_ORDER = [
GENERAL_CATEGORY,
@@ -39,4 +40,5 @@ export const CATEGORY_ORDER = [
SECURITY_SOLUTION_CATEGORY,
TIMELION_CATEGORY,
VISUALIZATION_CATEGORY,
+ DEV_TOOLS_CATEGORY,
];
diff --git a/packages/kbn-management/settings/utilities/category/get_category_name.ts b/packages/kbn-management/settings/utilities/category/get_category_name.ts
index 110d207bf5a6c..88756c34e412c 100644
--- a/packages/kbn-management/settings/utilities/category/get_category_name.ts
+++ b/packages/kbn-management/settings/utilities/category/get_category_name.ts
@@ -11,6 +11,7 @@ import {
ACCESSIBILITY_CATEGORY,
AUTOCOMPLETE_CATEGORY,
BANNER_CATEGORY,
+ DEV_TOOLS_CATEGORY,
DISCOVER_CATEGORY,
ENTERPRISE_SEARCH_CATEGORY,
GENERAL_CATEGORY,
@@ -92,6 +93,9 @@ const names: Record = {
[ROLLUPS_CATEGORY]: i18n.translate('management.settings.categoryNames.rollupsLabel', {
defaultMessage: 'Rollups',
}),
+ [DEV_TOOLS_CATEGORY]: i18n.translate('management.settings.categoryNames.devToolsLabel', {
+ defaultMessage: 'Developer Tools',
+ }),
};
export function getCategoryName(category?: string) {
diff --git a/packages/serverless/settings/search_project/index.ts b/packages/serverless/settings/search_project/index.ts
index a26c658501617..5a16690fd5f76 100644
--- a/packages/serverless/settings/search_project/index.ts
+++ b/packages/serverless/settings/search_project/index.ts
@@ -7,5 +7,9 @@
*/
import { COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX_ID } from '@kbn/management-settings-ids';
+import { ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from '@kbn/dev-tools-plugin/common';
-export const SEARCH_PROJECT_SETTINGS = [COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX_ID];
+export const SEARCH_PROJECT_SETTINGS = [
+ COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX_ID,
+ ENABLE_DOCKED_CONSOLE_UI_SETTING_ID,
+];
diff --git a/packages/serverless/settings/search_project/tsconfig.json b/packages/serverless/settings/search_project/tsconfig.json
index 16d6022e3d9bc..7d290d255a784 100644
--- a/packages/serverless/settings/search_project/tsconfig.json
+++ b/packages/serverless/settings/search_project/tsconfig.json
@@ -15,5 +15,6 @@
],
"kbn_references": [
"@kbn/management-settings-ids",
+ "@kbn/dev-tools-plugin",
]
}
diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts
index 53cb16befe865..ca097322486ea 100644
--- a/src/plugins/console/public/plugin.ts
+++ b/src/plugins/console/public/plugin.ts
@@ -7,6 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/public';
+import { ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from '@kbn/dev-tools-plugin/public';
import { renderEmbeddableConsole } from './application/containers/embeddable';
import {
@@ -109,10 +110,14 @@ export class ConsoleUIPlugin implements Plugin();
const consoleStart: ConsolePluginStart = {};
+ const embeddedConsoleUiSetting = core.uiSettings.get(
+ ENABLE_DOCKED_CONSOLE_UI_SETTING_ID
+ );
const embeddedConsoleAvailable =
isConsoleUiEnabled &&
isEmbeddedConsoleEnabled &&
- core.application.capabilities?.dev_tools?.show === true;
+ core.application.capabilities?.dev_tools?.show === true &&
+ embeddedConsoleUiSetting;
if (embeddedConsoleAvailable) {
consoleStart.renderEmbeddableConsole = (props?: EmbeddableConsoleProps) => {
diff --git a/src/plugins/dev_tools/common/constants.ts b/src/plugins/dev_tools/common/constants.ts
new file mode 100644
index 0000000000000..b5e7c9adde842
--- /dev/null
+++ b/src/plugins/dev_tools/common/constants.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/**
+ * The UI Setting prefix and category for dev tools UI Settings
+ */
+export const DEV_TOOLS_FEATURE_ID = 'devTools';
+/**
+ * UI Setting ID for enabling / disabling the docked console in Kibana
+ */
+export const ENABLE_DOCKED_CONSOLE_UI_SETTING_ID = 'devTools:enableDockedConsole';
diff --git a/src/plugins/dev_tools/common/index.ts b/src/plugins/dev_tools/common/index.ts
new file mode 100644
index 0000000000000..dc6ec6152e519
--- /dev/null
+++ b/src/plugins/dev_tools/common/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { DEV_TOOLS_FEATURE_ID, ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from './constants';
diff --git a/src/plugins/dev_tools/public/index.ts b/src/plugins/dev_tools/public/index.ts
index 9e685ee4ff507..03f0981a4a845 100644
--- a/src/plugins/dev_tools/public/index.ts
+++ b/src/plugins/dev_tools/public/index.ts
@@ -12,6 +12,7 @@
import { PluginInitializerContext } from '@kbn/core/public';
import { DevToolsPlugin } from './plugin';
export * from './plugin';
+export * from '../common/constants';
export function plugin(initializerContext: PluginInitializerContext) {
return new DevToolsPlugin(initializerContext);
diff --git a/src/plugins/dev_tools/server/plugin.ts b/src/plugins/dev_tools/server/plugin.ts
index f4157691e2a87..dbb4098f74e79 100644
--- a/src/plugins/dev_tools/server/plugin.ts
+++ b/src/plugins/dev_tools/server/plugin.ts
@@ -6,12 +6,17 @@
* Side Public License, v 1.
*/
-import { PluginInitializerContext, Plugin } from '@kbn/core/server';
+import { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server';
+import { uiSettings } from './ui_settings';
export class DevToolsServerPlugin implements Plugin {
constructor(initializerContext: PluginInitializerContext) {}
- public setup() {
+ public setup(core: CoreSetup) {
+ /**
+ * Register Dev Tools UI Settings
+ */
+ core.uiSettings.register(uiSettings);
return {};
}
diff --git a/src/plugins/dev_tools/server/ui_settings.ts b/src/plugins/dev_tools/server/ui_settings.ts
new file mode 100644
index 0000000000000..ff7d04d847b46
--- /dev/null
+++ b/src/plugins/dev_tools/server/ui_settings.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from '@kbn/core/types';
+import { i18n } from '@kbn/i18n';
+
+import { DEV_TOOLS_FEATURE_ID, ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from '../common/constants';
+
+/**
+ * uiSettings definitions for Dev Tools
+ */
+export const uiSettings: Record> = {
+ [ENABLE_DOCKED_CONSOLE_UI_SETTING_ID]: {
+ category: [DEV_TOOLS_FEATURE_ID],
+ description: i18n.translate('devTools.uiSettings.dockedConsole.description', {
+ defaultMessage:
+ 'Docks the Console in the Kibana UI. This setting does not affect the standard Console in Dev Tools.',
+ }),
+ name: i18n.translate('devTools.uiSettings.dockedConsole.name', {
+ defaultMessage: 'Docked Console',
+ }),
+ requiresPageReload: true,
+ schema: schema.boolean(),
+ value: true,
+ },
+};
diff --git a/src/plugins/dev_tools/tsconfig.json b/src/plugins/dev_tools/tsconfig.json
index b8dee30d16e73..13e30f92c8d6e 100644
--- a/src/plugins/dev_tools/tsconfig.json
+++ b/src/plugins/dev_tools/tsconfig.json
@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "target/types",
},
- "include": ["public/**/*", "server/**/*"],
+ "include": ["common/*", "public/**/*", "server/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/url-forwarding-plugin",
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
index 12e419822c5c8..1269a68e41c17 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -628,4 +628,8 @@ export const stackManagementSchema: MakeSchemaFrom = {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },
},
+ 'devTools:enableDockedConsole': {
+ type: 'boolean',
+ _meta: { description: 'Non-default value of setting.' },
+ },
};
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
index f850103afb14b..177a01b81af19 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -166,4 +166,5 @@ export interface UsageStats {
'observability:profilingCostPervCPUPerHour': number;
'observability:profilingAWSCostDiscountRate': number;
'data_views:fields_excluded_data_tiers': string;
+ 'devTools:enableDockedConsole': boolean;
}
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 659c791645ca9..b5108f3d9269d 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -10257,6 +10257,12 @@
"_meta": {
"description": "Non-default value of setting."
}
+ },
+ "devTools:enableDockedConsole": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Non-default value of setting."
+ }
}
}
},
From 1bd88ff98726fde98fa71e8931eabbf38ece00f7 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Thu, 8 Feb 2024 13:34:51 -0700
Subject: [PATCH 049/104] [maps] ES|QL adhoc data view (#176310)
Fixes https://github.com/elastic/kibana/issues/176325
https://github.com/elastic/kibana/pull/174736 resolved the issue of
[Filter bar displays repeated same dataview names equal to the number of
ES|QL panels based on that dataview in the
dashboard](https://github.com/elastic/kibana/issues/168131). This PR
uses `getESQLAdHocDataview` to create an adhoc data view for ESQL
statements.
### Test steps
1. install sample web logs data set. Wait for install to finish (so web
logs is default data view)
2. install sample flights data set.
3. create new map
4. add ESQL layer. Set statement to `from kibana_sample_data_flights |
keep DestLocation | limit 10000`. Click run.
5. Verify data view is passed to unified search and flights fields are
displayed in typeahead in unified search
6. save map
7. open map in new tab. Verify adhoc data view is re-hydrated in new tab
and unified search works as expected
---
.../source_descriptor_types.ts | 1 +
.../esql_source/create_source_editor.test.tsx | 22 +++++++--
.../esql_source/create_source_editor.tsx | 24 +++++++---
.../sources/esql_source/esql_editor.tsx | 8 +---
.../sources/esql_source/esql_source.test.ts | 3 ++
.../sources/esql_source/esql_source.tsx | 9 ++++
.../classes/sources/esql_source/esql_utils.ts | 47 ++++++++-----------
.../esql_source/update_source_editor.test.tsx | 43 +++++++++++++----
.../esql_source/update_source_editor.tsx | 8 +++-
.../fixtures/kbn_archiver/maps.json | 4 +-
10 files changed, 109 insertions(+), 60 deletions(-)
diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts
index b8979103f50d8..ebbbfd3fde1c1 100644
--- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts
@@ -45,6 +45,7 @@ export type ESQLSourceDescriptor = AbstractSourceDescriptor & {
id: string;
esql: string;
columns: ESQLColumn[];
+ dataViewId: string;
/*
* Date field used to narrow ES|QL requests by global time range
*/
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx
index eba948dec895f..cb7206e5bab7c 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx
@@ -7,10 +7,12 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
+import type { DataViewSpec } from '@kbn/data-plugin/common';
import { CreateSourceEditor } from './create_source_editor';
jest.mock('../../../kibana_services', () => {
- const mockDefaultDataView = {
+ const DEFAULT_DATA_VIEW_INDEX_PATTERN = 'logs';
+ const defaultDataView = {
fields: [
{
name: 'location',
@@ -23,10 +25,11 @@ jest.mock('../../../kibana_services', () => {
],
timeFieldName: '@timestamp',
getIndexPattern: () => {
- return 'logs';
+ return DEFAULT_DATA_VIEW_INDEX_PATTERN;
},
};
- const mockDataView = {
+
+ const otherDataView = {
fields: [
{
name: 'geometry',
@@ -37,14 +40,21 @@ jest.mock('../../../kibana_services', () => {
return 'world_countries';
},
};
+
return {
getIndexPatternService() {
return {
+ create: async (spec: DataViewSpec) => {
+ return {
+ ...(spec.title === DEFAULT_DATA_VIEW_INDEX_PATTERN ? defaultDataView : otherDataView),
+ id: spec.id,
+ };
+ },
get: async () => {
- return mockDataView;
+ return otherDataView;
},
getDefaultDataView: async () => {
- return mockDefaultDataView;
+ return defaultDataView;
},
};
},
@@ -63,6 +73,7 @@ describe('CreateSourceEditor', () => {
type: 'geo_point',
},
],
+ dataViewId: '30de729e173668cbf8954aa56c4aca5b82a1005586a608b692dae478219f8c76',
dateField: '@timestamp',
esql: 'from logs | keep location | limit 10000',
geoField: 'location',
@@ -89,6 +100,7 @@ describe('CreateSourceEditor', () => {
type: 'geo_shape',
},
],
+ dataViewId: 'c9f096614a62aa31893a2d6e8f43139bda7dcdb262b9373f79d0173cc152b4a4',
dateField: undefined,
esql: 'from world_countries | keep geometry | limit 10000',
geoField: 'geometry',
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx
index 4c75246aed567..4533c54d1a1ce 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx
@@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import type { ESQLColumn } from '@kbn/es-types';
+import { getESQLAdHocDataview } from '@kbn/esql-utils';
import {
EuiFormRow,
EuiPanel,
@@ -32,6 +33,7 @@ interface Props {
export function CreateSourceEditor(props: Props) {
const [isInitialized, setIsInitialized] = useState(false);
+ const [adhocDataViewId, setAdhocDataViewId] = useState();
const [columns, setColumns] = useState([]);
const [esql, setEsql] = useState('');
const [dateField, setDateField] = useState();
@@ -52,17 +54,20 @@ export function CreateSourceEditor(props: Props) {
}
getDataView()
- .then((dataView) => {
+ .then(async (dataView) => {
+ const adhocDataView = dataView
+ ? await getESQLAdHocDataview(dataView.getIndexPattern(), getIndexPatternService())
+ : undefined;
if (ignore) {
return;
}
- if (dataView) {
+ if (adhocDataView) {
let initialGeoField: DataViewField | undefined;
const initialDateFields: string[] = [];
const initialGeoFields: string[] = [];
- for (let i = 0; i < dataView.fields.length; i++) {
- const field = dataView.fields[i];
+ for (let i = 0; i < adhocDataView.fields.length; i++) {
+ const field = adhocDataView.fields[i];
if (
[ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE].includes(
field.type as ES_GEO_FIELD_TYPE
@@ -77,12 +82,13 @@ export function CreateSourceEditor(props: Props) {
if (initialGeoField) {
let initialDateField: string | undefined;
- if (dataView.timeFieldName) {
+ // get default time field from default data view instead of adhoc data view
+ if (dataView?.timeFieldName) {
initialDateField = dataView.timeFieldName;
} else if (initialDateFields.length) {
initialDateField = initialDateFields[0];
}
- const initialEsql = `from ${dataView.getIndexPattern()} | keep ${
+ const initialEsql = `from ${adhocDataView.getIndexPattern()} | keep ${
initialGeoField.name
} | limit 10000`;
setColumns([
@@ -94,6 +100,7 @@ export function CreateSourceEditor(props: Props) {
: ESQL_GEO_POINT_TYPE,
},
]);
+ setAdhocDataViewId(adhocDataView.id);
setDateField(initialDateField);
setDateFields(initialDateFields);
setGeoField(initialGeoField.name);
@@ -123,9 +130,10 @@ export function CreateSourceEditor(props: Props) {
useDebounce(
() => {
const sourceConfig =
- esql && esql.length
+ esql && esql.length && adhocDataViewId
? {
columns,
+ dataViewId: adhocDataViewId,
dateField,
geoField,
esql,
@@ -138,6 +146,7 @@ export function CreateSourceEditor(props: Props) {
},
0,
[
+ adhocDataViewId,
columns,
dateField,
geoField,
@@ -154,6 +163,7 @@ export function CreateSourceEditor(props: Props) {
{
+ setAdhocDataViewId(change.adhocDataViewId);
setColumns(change.columns);
setEsql(change.esql);
setDateFields(change.dateFields);
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx
index 0145dc8239273..8ed04f61adb5d 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx
@@ -17,11 +17,13 @@ import { getESQLMeta, verifyGeometryColumn } from './esql_utils';
interface Props {
esql: string;
onESQLChange: ({
+ adhocDataViewId,
columns,
dateFields,
geoFields,
esql,
}: {
+ adhocDataViewId: string;
columns: ESQLColumn[];
dateFields: string[];
geoFields: string[];
@@ -81,12 +83,6 @@ export function ESQLEditor(props: Props) {
return;
}
setError(err);
- props.onESQLChange({
- columns: [],
- dateFields: [],
- geoFields: [],
- esql: '',
- });
}
setIsLoading(false);
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts
index 42446ad8cb7e4..eabaedf681b2c 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts
@@ -11,6 +11,7 @@ import { VECTOR_SHAPE_TYPE } from '../../../../common/constants';
describe('getSupportedShapeTypes', () => {
test('should return point for geo_point column', async () => {
const descriptor = ESQLSource.createDescriptor({
+ dataViewId: '1234',
esql: 'from kibana_sample_data_logs | keep geo.coordinates | limit 10000',
columns: [
{
@@ -25,6 +26,7 @@ describe('getSupportedShapeTypes', () => {
test('should return all geometry types for geo_shape column', async () => {
const descriptor = ESQLSource.createDescriptor({
+ dataViewId: '1234',
esql: 'from world_countries | keep geometry | limit 10000',
columns: [
{
@@ -43,6 +45,7 @@ describe('getSupportedShapeTypes', () => {
test('should fallback to point when geometry column can not be found', async () => {
const descriptor = ESQLSource.createDescriptor({
+ dataViewId: '1234',
esql: 'from world_countries | keep geometry | limit 10000',
});
const esqlSource = new ESQLSource(descriptor);
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx
index d438a714beb40..53745e7426b70 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx
@@ -52,12 +52,17 @@ export class ESQLSource extends AbstractVectorSource implements IVectorSource {
throw new Error('Cannot create ESQLSourceDescriptor when esql is not provided');
}
+ if (!isValidStringConfig(descriptor.dataViewId)) {
+ throw new Error('Cannot create ESQLSourceDescriptor when dataViewId is not provided');
+ }
+
return {
...descriptor,
id: isValidStringConfig(descriptor.id) ? descriptor.id! : uuidv4(),
type: SOURCE_TYPES.ESQL,
esql: descriptor.esql!,
columns: descriptor.columns ? descriptor.columns : [],
+ dataViewId: descriptor.dataViewId!,
narrowByGlobalSearch:
typeof descriptor.narrowByGlobalSearch !== 'undefined'
? descriptor.narrowByGlobalSearch
@@ -316,4 +321,8 @@ export class ESQLSource extends AbstractVectorSource implements IVectorSource {
narrowByGlobalTime: this._descriptor.narrowByGlobalTime,
};
}
+
+ getIndexPatternId() {
+ return this._descriptor.dataViewId;
+ }
}
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts
index c247170874ba3..144516f7db5d0 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts
@@ -7,7 +7,8 @@
import { i18n } from '@kbn/i18n';
import { lastValueFrom } from 'rxjs';
-import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
+import type { DataView } from '@kbn/data-plugin/common';
+import { getESQLAdHocDataview, getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import type { ESQLColumn, ESQLSearchReponse } from '@kbn/es-types';
import { ES_GEO_FIELD_TYPE } from '../../../../common/constants';
import { getData, getIndexPatternService } from '../../../kibana_services';
@@ -49,10 +50,14 @@ export function verifyGeometryColumn(columns: ESQLColumn[]) {
}
export async function getESQLMeta(esql: string) {
- const fields = await getFields(esql);
+ const adhocDataView = await getESQLAdHocDataview(
+ getIndexPatternFromESQLQuery(esql),
+ getIndexPatternService()
+ );
return {
columns: await getColumns(esql),
- ...fields,
+ adhocDataViewId: adhocDataView.id!,
+ ...getFields(adhocDataView),
};
}
@@ -105,33 +110,19 @@ async function getColumns(esql: string) {
}
}
-export async function getFields(esql: string) {
+export function getFields(dataView: DataView) {
const dateFields: string[] = [];
const geoFields: string[] = [];
- const pattern: string = getIndexPatternFromESQLQuery(esql);
- try {
- // TODO pass field type filter to getFieldsForWildcard when field type filtering is supported
- (await getIndexPatternService().getFieldsForWildcard({ pattern })).forEach((field) => {
- if (field.type === 'date') {
- dateFields.push(field.name);
- } else if (
- field.type === ES_GEO_FIELD_TYPE.GEO_POINT ||
- field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE
- ) {
- geoFields.push(field.name);
- }
- });
- } catch (error) {
- throw new Error(
- i18n.translate('xpack.maps.source.esql.getFieldsErrorMsg', {
- defaultMessage: `Unable to load fields from index pattern: {pattern}. {errorMessage}`,
- values: {
- errorMessage: error.message,
- pattern,
- },
- })
- );
- }
+ dataView.fields.forEach((field) => {
+ if (field.type === 'date') {
+ dateFields.push(field.name);
+ } else if (
+ field.type === ES_GEO_FIELD_TYPE.GEO_POINT ||
+ field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE
+ ) {
+ geoFields.push(field.name);
+ }
+ });
return {
dateFields,
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx
index 7c6f9f34b1f56..7491f6bc2b049 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx
@@ -11,19 +11,38 @@ import userEvent from '@testing-library/user-event';
import { UpdateSourceEditor } from './update_source_editor';
import { ESQLSource } from './esql_source';
-jest.mock('./esql_utils', () => ({}));
-
-describe('UpdateSourceEditor', () => {
- beforeEach(() => {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- require('./esql_utils').getFields = async () => {
+jest.mock('../../../kibana_services', () => {
+ return {
+ getIndexPatternService() {
return {
- dateFields: ['timestamp', 'utc_timestamp'],
- geoFields: ['location', 'dest_location'],
+ get: async () => {
+ return {
+ fields: [
+ {
+ name: 'timestamp',
+ type: 'date',
+ },
+ {
+ name: 'utc_timestamp',
+ type: 'date',
+ },
+ {
+ name: 'location',
+ type: 'geo_point',
+ },
+ {
+ name: 'utc_timestamp',
+ type: 'geo_point',
+ },
+ ],
+ };
+ },
};
- };
- });
+ },
+ };
+});
+describe('UpdateSourceEditor', () => {
describe('narrow by map bounds switch', () => {
function getNarrowByMapBoundsSwitch() {
return screen.getByText('Narrow ES|QL statement by visible map area');
@@ -32,6 +51,7 @@ describe('UpdateSourceEditor', () => {
test('should set geoField when checked and geo field is not set', async () => {
const onChange = jest.fn();
const sourceDescriptor = ESQLSource.createDescriptor({
+ dataViewId: '1234',
esql: 'from logs | keep location | limit 10000',
columns: [
{
@@ -55,6 +75,7 @@ describe('UpdateSourceEditor', () => {
test('should not reset geoField when checked and geoField is set', async () => {
const onChange = jest.fn();
const sourceDescriptor = ESQLSource.createDescriptor({
+ dataViewId: '1234',
esql: 'from logs | keep location | limit 10000',
columns: [
{
@@ -82,6 +103,7 @@ describe('UpdateSourceEditor', () => {
test('should set dateField when checked and date field is not set', async () => {
const onChange = jest.fn();
const sourceDescriptor = ESQLSource.createDescriptor({
+ dataViewId: '1234',
esql: 'from logs | keep location | limit 10000',
columns: [
{
@@ -105,6 +127,7 @@ describe('UpdateSourceEditor', () => {
test('should not reset dateField when checked and dateField is set', async () => {
const onChange = jest.fn();
const sourceDescriptor = ESQLSource.createDescriptor({
+ dataViewId: '1234',
esql: 'from logs | keep location | limit 10000',
columns: [
{
diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx
index 1a147cf93085f..e77bcf862f929 100644
--- a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx
@@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n';
import type { ESQLSourceDescriptor } from '../../../../common/descriptor_types';
import type { OnSourceChangeArgs } from '../source';
import { ForceRefreshCheckbox } from '../../../components/force_refresh_checkbox';
+import { getIndexPatternService } from '../../../kibana_services';
import { ESQLEditor } from './esql_editor';
import { NarrowByMapBounds, NarrowByTime } from './narrow_by_field';
import { getFields } from './esql_utils';
@@ -35,11 +36,13 @@ export function UpdateSourceEditor(props: Props) {
useEffect(() => {
let ignore = false;
- getFields(props.sourceDescriptor.esql)
- .then((fields) => {
+ getIndexPatternService()
+ .get(props.sourceDescriptor.dataViewId)
+ .then((dataView) => {
if (ignore) {
return;
}
+ const fields = getFields(dataView);
setDateFields(fields.dateFields);
setGeoFields(fields.geoFields);
setIsInitialized(true);
@@ -79,6 +82,7 @@ export function UpdateSourceEditor(props: Props) {
setGeoFields(change.geoFields);
const changes: OnSourceChangeArgs[] = [
{ propName: 'columns', value: change.columns },
+ { propName: 'dataViewId', value: change.adhocDataViewId },
{ propName: 'esql', value: change.esql },
];
function ensureField(key: 'dateField' | 'geoField', fields: string[]) {
diff --git a/x-pack/test/functional/fixtures/kbn_archiver/maps.json b/x-pack/test/functional/fixtures/kbn_archiver/maps.json
index 1ce5bc3fd6aa1..b7fef81c49bfb 100644
--- a/x-pack/test/functional/fixtures/kbn_archiver/maps.json
+++ b/x-pack/test/functional/fixtures/kbn_archiver/maps.json
@@ -1181,8 +1181,8 @@
"attributes": {
"title": "esql example",
"description": "",
- "mapStateJSON": "{\"adHocDataViews\":[],\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
- "layerListJSON": "[{\"sourceDescriptor\":{\"columns\":[{\"name\":\"geo.coordinates\",\"type\":\"geo_point\"}],\"dateField\":\"@timestamp\",\"esql\":\"from logstash-* | KEEP geo.coordinates | limit 10000\",\"id\":\"fad0e2eb-9278-415c-bdc8-1189a46eac0b\",\"type\":\"ESQL\",\"narrowByGlobalSearch\":true,\"narrowByMapBounds\":true,\"applyForceRefresh\":true,\"geoField\":\"geo.coordinates\",\"narrowByGlobalTime\":true},\"id\":\"59ca05b3-e3be-4fb4-ab4d-56c17b8bd589\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]",
+ "mapStateJSON": "{\"adHocDataViews\":[{\"id\":\"624b9c7c17e840dd2161e64c37fe4696eb042ca3afbba2c390c742e3f576a8c9\",\"title\":\"logstash-*\",\"sourceFilters\":[],\"fieldFormats\":{},\"runtimeFieldMap\":{},\"fieldAttrs\":{},\"allowNoIndex\":false,\"name\":\"logstash-*\",\"allowHidden\":false}],\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
+ "layerListJSON": "[{\"sourceDescriptor\":{\"columns\":[{\"name\":\"geo.coordinates\",\"type\":\"geo_point\"}],\"dataViewId\":\"624b9c7c17e840dd2161e64c37fe4696eb042ca3afbba2c390c742e3f576a8c9\",\"dateField\":\"@timestamp\",\"esql\":\"from logstash-* | KEEP geo.coordinates | limit 10000\",\"id\":\"fad0e2eb-9278-415c-bdc8-1189a46eac0b\",\"type\":\"ESQL\",\"narrowByGlobalSearch\":true,\"narrowByMapBounds\":true,\"applyForceRefresh\":true,\"geoField\":\"geo.coordinates\",\"narrowByGlobalTime\":true},\"id\":\"59ca05b3-e3be-4fb4-ab4d-56c17b8bd589\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]",
"uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
},
"references": [],
From e3f1d1272b4d0d5d4097417dd010ca71da7b25ae Mon Sep 17 00:00:00 2001
From: Adam Demjen
Date: Thu, 8 Feb 2024 15:36:00 -0500
Subject: [PATCH 050/104] [Search] Consolidate ML model fetch calls (#176257)
## Summary
With the introduction of
[fetch_ml_models.ts](https://github.com/elastic/kibana/blob/main/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts),
the fetching and enriching of ML models for Search purposes has been
consolidated in that API. This allows us to remove the dependency on the
older method that works with ML plugin-specific `TrainedModel` entities.
This PR makes the following changes:
- Switch over code that depend on ML models to use the new function from
`fetch_ml_models.ts` (that already does sorting/filtering).
- Move the fetch process to `ml_inference_logic.ts`, and begin
periodically polling after mounting the logic. This enables passing down
values to lower components, e.g. `model_select_logic.ts`, instead of
repeating the fetch there.
- Use `MlModel` instead of `TrainedModel/MlTrainedModelConfig`. This
requires adding some missing properties to `MlModel`: `types`,
`inputFieldNames`, `version`.
- Remove the old fetch methods
(`x-pack/plugins/enterprise_search/server/lib/ml/ml_*_logic.ts`).
- Remove the "no models available" component and condition, since as of
8.12 at least the ELSER/E5 placeholders are always present.
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../ml_inference_pipeline/index.test.ts | 38 ++-
.../common/ml_inference_pipeline/index.ts | 12 +-
.../enterprise_search/common/types/ml.ts | 5 +
.../cached_fetch_models_api_logic.test.ts | 8 +-
.../cached_fetch_models_api_logic.ts | 2 +
.../ml_models/ml_model_stats_logic.test.ts | 25 --
.../api/ml_models/ml_model_stats_logic.ts | 35 --
.../api/ml_models/ml_models_logic.test.ts | 28 --
.../api/ml_models/ml_models_logic.ts | 31 --
.../ml_models/ml_trained_models_logic.test.ts | 177 ----------
.../api/ml_models/ml_trained_models_logic.ts | 169 ----------
.../add_inference_pipeline_flyout.test.tsx | 6 -
.../add_inference_pipeline_flyout.tsx | 13 +-
.../ml_inference/inference_config.tsx | 7 +-
.../ml_inference/ml_inference_logic.test.ts | 315 +++++++-----------
.../ml_inference/ml_inference_logic.ts | 104 +++---
.../ml_inference/model_select.test.tsx | 3 +
.../ml_inference/model_select_logic.test.ts | 36 +-
.../ml_inference/model_select_logic.ts | 61 ++--
.../ml_inference/model_select_option.test.tsx | 3 +
.../pipelines/ml_inference/no_models.tsx | 57 ----
.../shared/ml_inference/utils.test.ts | 81 +----
.../components/shared/ml_inference/utils.ts | 23 --
.../server/lib/ml/fetch_ml_models.test.ts | 51 +++
.../server/lib/ml/fetch_ml_models.ts | 67 ++--
.../enterprise_search/server/lib/ml/utils.ts | 2 +
.../translations/translations/fr-FR.json | 3 -
.../translations/translations/ja-JP.json | 3 -
.../translations/translations/zh-CN.json | 3 -
29 files changed, 345 insertions(+), 1023 deletions(-)
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.test.ts
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.ts
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.test.ts
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.test.ts
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.ts
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
diff --git a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts
index 78af0a862d302..93f8152efa19b 100644
--- a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts
+++ b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts
@@ -8,6 +8,7 @@
import { MlTrainedModelConfig, MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types';
import { BUILT_IN_MODEL_TAG, TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils';
+import { MlModel, MlModelDeploymentState } from '../types/ml';
import { MlInferencePipeline, TrainedModelState } from '../types/pipelines';
import {
@@ -19,7 +20,7 @@ import {
parseModelStateReasonFromStats,
} from '.';
-const mockModel: MlTrainedModelConfig = {
+const mockTrainedModel: MlTrainedModelConfig = {
inference_config: {
ner: {},
},
@@ -32,8 +33,27 @@ const mockModel: MlTrainedModelConfig = {
version: '1',
};
+const mockModel: MlModel = {
+ modelId: 'model_1',
+ type: 'ner',
+ title: 'Model 1',
+ description: 'Model 1 description',
+ licenseType: 'elastic',
+ modelDetailsPageUrl: 'https://my-model.ai',
+ deploymentState: MlModelDeploymentState.NotDeployed,
+ startTime: 0,
+ targetAllocationCount: 0,
+ nodeAllocationCount: 0,
+ threadsPerAllocation: 0,
+ isPlaceholder: false,
+ hasStats: false,
+ types: ['pytorch', 'ner'],
+ inputFieldNames: ['title'],
+ version: '1',
+};
+
describe('getMlModelTypesForModelConfig lib function', () => {
- const builtInMockModel: MlTrainedModelConfig = {
+ const builtInMockTrainedModel: MlTrainedModelConfig = {
inference_config: {
text_classification: {},
},
@@ -47,13 +67,13 @@ describe('getMlModelTypesForModelConfig lib function', () => {
it('should return the model type and inference config type', () => {
const expected = ['pytorch', 'ner'];
- const response = getMlModelTypesForModelConfig(mockModel);
+ const response = getMlModelTypesForModelConfig(mockTrainedModel);
expect(response.sort()).toEqual(expected.sort());
});
it('should include the built in type', () => {
const expected = ['lang_ident', 'text_classification', BUILT_IN_MODEL_TAG];
- const response = getMlModelTypesForModelConfig(builtInMockModel);
+ const response = getMlModelTypesForModelConfig(builtInMockTrainedModel);
expect(response.sort()).toEqual(expected.sort());
});
});
@@ -71,9 +91,9 @@ describe('generateMlInferencePipelineBody lib function', () => {
{
inference: {
field_map: {
- 'my-source-field': 'MODEL_INPUT_FIELD',
+ 'my-source-field': 'title',
},
- model_id: 'test_id',
+ model_id: 'model_1',
on_failure: [
{
append: {
@@ -154,21 +174,21 @@ describe('generateMlInferencePipelineBody lib function', () => {
{
inference: expect.objectContaining({
field_map: {
- 'my-source-field1': 'MODEL_INPUT_FIELD',
+ 'my-source-field1': 'title',
},
}),
},
{
inference: expect.objectContaining({
field_map: {
- 'my-source-field2': 'MODEL_INPUT_FIELD',
+ 'my-source-field2': 'title',
},
}),
},
{
inference: expect.objectContaining({
field_map: {
- 'my-source-field3': 'MODEL_INPUT_FIELD',
+ 'my-source-field3': 'title',
},
}),
},
diff --git a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts
index 5f56c1105b297..fa16dd29f83b1 100644
--- a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts
+++ b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts
@@ -18,6 +18,8 @@ import {
BUILT_IN_MODEL_TAG,
} from '@kbn/ml-trained-models-utils';
+import { MlModel } from '../types/ml';
+
import {
MlInferencePipeline,
CreateMLInferencePipeline,
@@ -33,7 +35,7 @@ export interface MlInferencePipelineParams {
description?: string;
fieldMappings: FieldMapping[];
inferenceConfig?: InferencePipelineInferenceConfig;
- model: MlTrainedModelConfig;
+ model: MlModel;
pipelineName: string;
}
@@ -90,7 +92,7 @@ export const generateMlInferencePipelineBody = ({
model_version: model.version,
pipeline: pipelineName,
processed_timestamp: '{{{ _ingest.timestamp }}}',
- types: getMlModelTypesForModelConfig(model),
+ types: model.types,
},
],
},
@@ -104,19 +106,19 @@ export const getInferenceProcessor = (
sourceField: string,
targetField: string,
inferenceConfig: InferencePipelineInferenceConfig | undefined,
- model: MlTrainedModelConfig,
+ model: MlModel,
pipelineName: string
): IngestInferenceProcessor => {
// If model returned no input field, insert a placeholder
const modelInputField =
- model.input?.field_names?.length > 0 ? model.input.field_names[0] : 'MODEL_INPUT_FIELD';
+ model.inputFieldNames.length > 0 ? model.inputFieldNames[0] : 'MODEL_INPUT_FIELD';
return {
field_map: {
[sourceField]: modelInputField,
},
inference_config: inferenceConfig,
- model_id: model.model_id,
+ model_id: model.modelId,
on_failure: [
{
append: {
diff --git a/x-pack/plugins/enterprise_search/common/types/ml.ts b/x-pack/plugins/enterprise_search/common/types/ml.ts
index 894ffa6f0726b..2f40475535107 100644
--- a/x-pack/plugins/enterprise_search/common/types/ml.ts
+++ b/x-pack/plugins/enterprise_search/common/types/ml.ts
@@ -27,6 +27,10 @@ export interface MlModel {
modelId: string;
/** Model inference type, e.g. ner, text_classification */
type: string;
+ /** Type-related tags: model type (e.g. pytorch), inference type, built-in tag */
+ types: string[];
+ /** Field names in inference input configuration */
+ inputFieldNames: string[];
title: string;
description?: string;
licenseType?: string;
@@ -44,4 +48,5 @@ export interface MlModel {
isPlaceholder: boolean;
/** Does this model have deployment stats? */
hasStats: boolean;
+ version?: string;
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts
index 6d66ed5704721..869bd9273ac09 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts
@@ -30,8 +30,11 @@ const DEFAULT_VALUES: CachedFetchModelsApiLogicValues = {
const FETCH_MODELS_API_DATA_RESPONSE: MlModel[] = [
{
modelId: 'model_1',
- title: 'Model 1',
type: 'ner',
+ title: 'Model 1',
+ description: 'Model 1 description',
+ licenseType: 'elastic',
+ modelDetailsPageUrl: 'https://my-model.ai',
deploymentState: MlModelDeploymentState.NotDeployed,
startTime: 0,
targetAllocationCount: 0,
@@ -39,6 +42,9 @@ const FETCH_MODELS_API_DATA_RESPONSE: MlModel[] = [
threadsPerAllocation: 0,
isPlaceholder: false,
hasStats: false,
+ types: ['pytorch', 'ner'],
+ inputFieldNames: ['title'],
+ version: '1',
},
];
const FETCH_MODELS_API_ERROR_RESPONSE = {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts
index d65af6ec2fcf4..a26dbada96c08 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts
@@ -18,6 +18,8 @@ import { FetchModelsApiLogic, FetchModelsApiResponse } from './fetch_models_api_
const FETCH_MODELS_POLLING_DURATION = 5000; // 5 seconds
const FETCH_MODELS_POLLING_DURATION_ON_FAILURE = 30000; // 30 seconds
+export type { FetchModelsApiResponse } from './fetch_models_api_logic';
+
export interface CachedFetchModlesApiLogicActions {
apiError: Actions<{}, FetchModelsApiResponse>['apiError'];
apiReset: Actions<{}, FetchModelsApiResponse>['apiReset'];
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.test.ts
deleted file mode 100644
index 4bcea4ac4e83a..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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 { mockHttpValues } from '../../../__mocks__/kea_logic';
-import { mlModelStats } from '../../__mocks__/ml_models.mock';
-
-import { getMLModelsStats } from './ml_model_stats_logic';
-
-describe('MLModelsApiLogic', () => {
- const { http } = mockHttpValues;
- beforeEach(() => {
- jest.clearAllMocks();
- });
- describe('getMLModelsStats', () => {
- it('calls the ml api', async () => {
- http.get.mockResolvedValue(mlModelStats);
- const result = await getMLModelsStats();
- expect(http.get).toHaveBeenCalledWith('/internal/ml/trained_models/_stats', { version: '1' });
- expect(result).toEqual(mlModelStats);
- });
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.ts
deleted file mode 100644
index d8bc341fcb6c3..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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 { MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types';
-
-import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
-import { HttpLogic } from '../../../shared/http';
-
-export type GetMlModelsStatsArgs = undefined;
-
-export interface GetMlModelsStatsResponse {
- count: number;
- trained_model_stats: MlTrainedModelStats[];
-}
-
-export const getMLModelsStats = async () => {
- return await HttpLogic.values.http.get(
- '/internal/ml/trained_models/_stats',
- { version: '1' }
- );
-};
-
-export const MLModelsStatsApiLogic = createApiLogic(
- ['ml_models_stats_api_logic'],
- getMLModelsStats,
- {
- clearFlashMessagesOnMakeRequest: false,
- showErrorFlash: false,
- }
-);
-
-export type MLModelsStatsApiLogicActions = Actions;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.test.ts
deleted file mode 100644
index cffdc08dfd2ef..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 { mockHttpValues } from '../../../__mocks__/kea_logic';
-import { mlModels } from '../../__mocks__/ml_models.mock';
-
-import { getMLModels } from './ml_models_logic';
-
-describe('MLModelsApiLogic', () => {
- const { http } = mockHttpValues;
- beforeEach(() => {
- jest.clearAllMocks();
- });
- describe('getMLModels', () => {
- it('calls the ml api', async () => {
- http.get.mockResolvedValue(mlModels);
- const result = await getMLModels();
- expect(http.get).toHaveBeenCalledWith('/internal/ml/trained_models', {
- query: { size: 1000, with_pipelines: true },
- version: '1',
- });
- expect(result).toEqual(mlModels);
- });
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts
deleted file mode 100644
index 9cec020b8d782..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models';
-
-import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
-import { HttpLogic } from '../../../shared/http';
-
-export type GetMlModelsArgs = number | undefined;
-
-export type GetMlModelsResponse = TrainedModelConfigResponse[];
-
-export const getMLModels = async (size: GetMlModelsArgs = 1000) => {
- return await HttpLogic.values.http.get(
- '/internal/ml/trained_models',
- {
- query: { size, with_pipelines: true },
- version: '1',
- }
- );
-};
-
-export const MLModelsApiLogic = createApiLogic(['ml_models_api_logic'], getMLModels, {
- clearFlashMessagesOnMakeRequest: false,
- showErrorFlash: false,
-});
-
-export type MLModelsApiLogicActions = Actions;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.test.ts
deleted file mode 100644
index 6a1a4e0e512df..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.test.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * 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 { LogicMounter } from '../../../__mocks__/kea_logic';
-import { mlModels, mlModelStats } from '../../__mocks__/ml_models.mock';
-
-import { HttpError, Status } from '../../../../../common/types/api';
-
-import { MLModelsStatsApiLogic } from './ml_model_stats_logic';
-import { MLModelsApiLogic } from './ml_models_logic';
-import { TrainedModelsApiLogic, TrainedModelsApiLogicValues } from './ml_trained_models_logic';
-
-const DEFAULT_VALUES: TrainedModelsApiLogicValues = {
- error: null,
- status: Status.IDLE,
- data: null,
- // models
- modelsApiStatus: {
- status: Status.IDLE,
- },
- modelsData: undefined,
- modelsApiError: undefined,
- modelsStatus: Status.IDLE,
- // stats
- modelStatsApiStatus: {
- status: Status.IDLE,
- },
- modelStatsData: undefined,
- modelsStatsApiError: undefined,
- modelStatsStatus: Status.IDLE,
-};
-
-describe('TrainedModelsApiLogic', () => {
- const { mount } = new LogicMounter(TrainedModelsApiLogic);
- const { mount: mountMLModelsApiLogic } = new LogicMounter(MLModelsApiLogic);
- const { mount: mountMLModelsStatsApiLogic } = new LogicMounter(MLModelsStatsApiLogic);
-
- beforeEach(() => {
- jest.clearAllMocks();
-
- mountMLModelsApiLogic();
- mountMLModelsStatsApiLogic();
- mount();
- });
-
- it('has default values', () => {
- expect(TrainedModelsApiLogic.values).toEqual(DEFAULT_VALUES);
- });
- describe('selectors', () => {
- describe('data', () => {
- it('returns combined trained models', () => {
- MLModelsApiLogic.actions.apiSuccess(mlModels);
- MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats);
-
- expect(TrainedModelsApiLogic.values.data).toEqual([
- {
- ...mlModels[0],
- ...mlModelStats.trained_model_stats[0],
- },
- {
- ...mlModels[1],
- ...mlModelStats.trained_model_stats[1],
- },
- {
- ...mlModels[2],
- ...mlModelStats.trained_model_stats[2],
- },
- ]);
- });
- it('returns just models if stats not available', () => {
- MLModelsApiLogic.actions.apiSuccess(mlModels);
-
- expect(TrainedModelsApiLogic.values.data).toEqual(mlModels);
- });
- it('returns null trained models even with stats if models missing', () => {
- MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats);
-
- expect(TrainedModelsApiLogic.values.data).toEqual(null);
- });
- });
- describe('error', () => {
- const modelError: HttpError = {
- body: {
- error: 'Model Error',
- statusCode: 400,
- },
- fetchOptions: {},
- request: {},
- } as HttpError;
- const statsError: HttpError = {
- body: {
- error: 'Stats Error',
- statusCode: 500,
- },
- fetchOptions: {},
- request: {},
- } as HttpError;
-
- it('returns null with no errors', () => {
- MLModelsApiLogic.actions.apiSuccess(mlModels);
- MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats);
-
- expect(TrainedModelsApiLogic.values.error).toBeNull();
- });
- it('models error', () => {
- MLModelsApiLogic.actions.apiError(modelError);
-
- expect(TrainedModelsApiLogic.values.error).toBe(modelError);
- });
- it('stats error', () => {
- MLModelsStatsApiLogic.actions.apiError(statsError);
-
- expect(TrainedModelsApiLogic.values.error).toBe(statsError);
- });
- it('prefers models error if both api calls fail', () => {
- MLModelsApiLogic.actions.apiError(modelError);
- MLModelsStatsApiLogic.actions.apiError(statsError);
-
- expect(TrainedModelsApiLogic.values.error).toBe(modelError);
- });
- });
- describe('status', () => {
- it('returns matching status for both calls', () => {
- MLModelsApiLogic.actions.apiSuccess(mlModels);
- MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats);
-
- expect(TrainedModelsApiLogic.values.status).toEqual(Status.SUCCESS);
- });
- it('returns models status when its lower', () => {
- MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats);
-
- expect(TrainedModelsApiLogic.values.status).toEqual(Status.IDLE);
- });
- it('returns stats status when its lower', () => {
- MLModelsApiLogic.actions.apiSuccess(mlModels);
-
- expect(TrainedModelsApiLogic.values.status).toEqual(Status.IDLE);
- });
- it('returns error status if one api call fails', () => {
- MLModelsApiLogic.actions.apiSuccess(mlModels);
- MLModelsStatsApiLogic.actions.apiError({
- body: {
- error: 'Stats Error',
- statusCode: 500,
- },
- fetchOptions: {},
- request: {},
- } as HttpError);
-
- expect(TrainedModelsApiLogic.values.status).toEqual(Status.ERROR);
- });
- });
- });
- describe('actions', () => {
- it('makeRequest fetches models and stats', () => {
- jest.spyOn(TrainedModelsApiLogic.actions, 'makeGetModelsRequest');
- jest.spyOn(TrainedModelsApiLogic.actions, 'makeGetModelsStatsRequest');
-
- TrainedModelsApiLogic.actions.makeRequest(undefined);
-
- expect(TrainedModelsApiLogic.actions.makeGetModelsRequest).toHaveBeenCalledTimes(1);
- expect(TrainedModelsApiLogic.actions.makeGetModelsStatsRequest).toHaveBeenCalledTimes(1);
- });
- it('apiReset resets both api logics', () => {
- jest.spyOn(TrainedModelsApiLogic.actions, 'getModelsApiReset');
- jest.spyOn(TrainedModelsApiLogic.actions, 'getModelsStatsApiReset');
-
- TrainedModelsApiLogic.actions.apiReset();
-
- expect(TrainedModelsApiLogic.actions.getModelsApiReset).toHaveBeenCalledTimes(1);
- expect(TrainedModelsApiLogic.actions.getModelsStatsApiReset).toHaveBeenCalledTimes(1);
- });
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.ts
deleted file mode 100644
index d36a80df6af6a..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * 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 { kea, MakeLogicType } from 'kea';
-
-import { MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types';
-import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models';
-
-import { ApiStatus, Status, HttpError } from '../../../../../common/types/api';
-import { Actions } from '../../../shared/api_logic/create_api_logic';
-
-import {
- GetMlModelsStatsResponse,
- MLModelsStatsApiLogic,
- MLModelsStatsApiLogicActions,
-} from './ml_model_stats_logic';
-import { GetMlModelsResponse, MLModelsApiLogic, MLModelsApiLogicActions } from './ml_models_logic';
-
-export type TrainedModel = TrainedModelConfigResponse & Partial;
-
-export type TrainedModelsApiLogicActions = Actions & {
- getModelsApiError: MLModelsApiLogicActions['apiError'];
- getModelsApiReset: MLModelsApiLogicActions['apiReset'];
- getModelsApiSuccess: MLModelsApiLogicActions['apiSuccess'];
- getModelsStatsApiError: MLModelsStatsApiLogicActions['apiError'];
- getModelsStatsApiReset: MLModelsStatsApiLogicActions['apiReset'];
- getModelsStatsApiSuccess: MLModelsStatsApiLogicActions['apiSuccess'];
- makeGetModelsRequest: MLModelsApiLogicActions['makeRequest'];
- makeGetModelsStatsRequest: MLModelsStatsApiLogicActions['makeRequest'];
-};
-export interface TrainedModelsApiLogicValues {
- error: HttpError | null;
- status: Status;
- data: TrainedModel[] | null;
- // models
- modelsApiStatus: ApiStatus;
- modelsData: GetMlModelsResponse | undefined;
- modelsApiError?: HttpError;
- modelsStatus: Status;
- // stats
- modelStatsApiStatus: ApiStatus;
- modelStatsData: GetMlModelsStatsResponse | undefined;
- modelsStatsApiError?: HttpError;
- modelStatsStatus: Status;
-}
-
-export const TrainedModelsApiLogic = kea<
- MakeLogicType
->({
- actions: {
- apiError: (error) => error,
- apiReset: true,
- apiSuccess: (result) => result,
- makeRequest: () => undefined,
- },
- connect: {
- actions: [
- MLModelsApiLogic,
- [
- 'apiError as getModelsApiError',
- 'apiReset as getModelsApiReset',
- 'apiSuccess as getModelsApiSuccess',
- 'makeRequest as makeGetModelsRequest',
- ],
- MLModelsStatsApiLogic,
- [
- 'apiError as getModelsStatsApiError',
- 'apiReset as getModelsStatsApiReset',
- 'apiSuccess as getModelsStatsApiSuccess',
- 'makeRequest as makeGetModelsStatsRequest',
- ],
- ],
- values: [
- MLModelsApiLogic,
- [
- 'apiStatus as modelsApiStatus',
- 'error as modelsApiError',
- 'status as modelsStatus',
- 'data as modelsData',
- ],
- MLModelsStatsApiLogic,
- [
- 'apiStatus as modelStatsApiStatus',
- 'error as modelsStatsApiError',
- 'status as modelStatsStatus',
- 'data as modelStatsData',
- ],
- ],
- },
- listeners: ({ actions, values }) => ({
- getModelsApiError: (error) => {
- actions.apiError(error);
- },
- getModelsApiSuccess: () => {
- if (!values.data) return;
- actions.apiSuccess(values.data);
- },
- getModelsStatsApiError: (error) => {
- if (values.modelsApiError) return;
- actions.apiError(error);
- },
- getModelsStatsApiSuccess: () => {
- if (!values.data) return;
- actions.apiSuccess(values.data);
- },
- apiReset: () => {
- actions.getModelsApiReset();
- actions.getModelsStatsApiReset();
- },
- makeRequest: () => {
- actions.makeGetModelsRequest(undefined);
- actions.makeGetModelsStatsRequest(undefined);
- },
- }),
- path: ['enterprise_search', 'api', 'ml_trained_models_api_logic'],
- selectors: ({ selectors }) => ({
- data: [
- () => [selectors.modelsData, selectors.modelStatsData],
- (
- modelsData: TrainedModelsApiLogicValues['modelsData'],
- modelStatsData: TrainedModelsApiLogicValues['modelStatsData']
- ): TrainedModel[] | null => {
- if (!modelsData) return null;
- if (!modelStatsData) return modelsData;
- const statsMap: Record =
- modelStatsData.trained_model_stats.reduce((map, value) => {
- if (value.model_id) {
- map[value.model_id] = value;
- }
- return map;
- }, {} as Record);
- return modelsData.map((modelConfig) => {
- const modelStats = statsMap[modelConfig.model_id];
- return {
- ...modelConfig,
- ...(modelStats ?? {}),
- };
- });
- },
- ],
- error: [
- () => [selectors.modelsApiStatus, selectors.modelStatsApiStatus],
- (
- modelsApiStatus: TrainedModelsApiLogicValues['modelsApiStatus'],
- modelStatsApiStatus: TrainedModelsApiLogicValues['modelStatsApiStatus']
- ) => {
- if (modelsApiStatus.error) return modelsApiStatus.error;
- if (modelStatsApiStatus.error) return modelStatsApiStatus.error;
- return null;
- },
- ],
- status: [
- () => [selectors.modelsApiStatus, selectors.modelStatsApiStatus],
- (
- modelsApiStatus: TrainedModelsApiLogicValues['modelsApiStatus'],
- modelStatsApiStatus: TrainedModelsApiLogicValues['modelStatsApiStatus']
- ) => {
- if (modelsApiStatus.status === modelStatsApiStatus.status) return modelsApiStatus.status;
- if (modelsApiStatus.status === Status.ERROR || modelStatsApiStatus.status === Status.ERROR)
- return Status.ERROR;
- if (modelsApiStatus.status < modelStatsApiStatus.status) return modelsApiStatus.status;
- return modelStatsApiStatus.status;
- },
- ],
- }),
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx
index 68bf7fc48e7dd..09789e34c963b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx
@@ -29,7 +29,6 @@ import {
import { ConfigureFields } from './configure_fields';
import { ConfigurePipeline } from './configure_pipeline';
import { EMPTY_PIPELINE_CONFIGURATION } from './ml_inference_logic';
-import { NoModelsPanel } from './no_models';
import { ReviewPipeline } from './review_pipeline';
import { TestPipeline } from './test_pipeline';
import { AddInferencePipelineSteps } from './types';
@@ -82,11 +81,6 @@ describe('AddInferencePipelineFlyout', () => {
const wrapper = shallow( );
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1);
});
- it('renders no models panel when there are no models', () => {
- setMockValues({ ...DEFAULT_VALUES, supportedMLModels: [] });
- const wrapper = shallow( );
- expect(wrapper.find(NoModelsPanel)).toHaveLength(1);
- });
it('renders AddInferencePipelineHorizontalSteps', () => {
const wrapper = shallow( );
expect(wrapper.find(AddInferencePipelineHorizontalSteps)).toHaveLength(1);
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx
index 654cdafad35eb..57aa7ab467488 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx
@@ -41,7 +41,6 @@ import { IndexViewLogic } from '../../index_view_logic';
import { ConfigureFields } from './configure_fields';
import { ConfigurePipeline } from './configure_pipeline';
import { MLInferenceLogic } from './ml_inference_logic';
-import { NoModelsPanel } from './no_models';
import { ReviewPipeline } from './review_pipeline';
import { TestPipeline } from './test_pipeline';
import { AddInferencePipelineSteps } from './types';
@@ -54,9 +53,15 @@ export interface AddInferencePipelineFlyoutProps {
export const AddInferencePipelineFlyout = (props: AddInferencePipelineFlyoutProps) => {
const { indexName } = useValues(IndexNameLogic);
- const { setIndexName } = useActions(MLInferenceLogic);
+ const { setIndexName, makeMlInferencePipelinesRequest, startPollingModels, makeMappingRequest } =
+ useActions(MLInferenceLogic);
useEffect(() => {
setIndexName(indexName);
+
+ // Trigger fetching of initial data: existing ML pipelines, available models, index mapping
+ makeMlInferencePipelinesRequest(undefined);
+ startPollingModels();
+ makeMappingRequest({ indexName });
}, [indexName]);
return (
@@ -82,7 +87,6 @@ export const AddInferencePipelineContent = ({ onClose }: AddInferencePipelineFly
const { ingestionMethod } = useValues(IndexViewLogic);
const {
createErrors,
- supportedMLModels,
isLoading,
addInferencePipelineModal: { step },
} = useValues(MLInferenceLogic);
@@ -103,9 +107,6 @@ export const AddInferencePipelineContent = ({ onClose }: AddInferencePipelineFly
);
}
- if (supportedMLModels.length === 0) {
- return ;
- }
return (
<>
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx
index 7b3fdf4353d99..244f7a3e8a200 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx
@@ -14,7 +14,6 @@ import { i18n } from '@kbn/i18n';
import { SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils';
-import { getMlModelTypesForModelConfig } from '../../../../../../../common/ml_inference_pipeline';
import { getMLType } from '../../../shared/ml_inference/utils';
import { MLInferenceLogic } from './ml_inference_logic';
@@ -23,10 +22,10 @@ import { ZeroShotClassificationInferenceConfiguration } from './zero_shot_infere
export const InferenceConfiguration: React.FC = () => {
const {
addInferencePipelineModal: { configuration },
- selectedMLModel,
+ selectedModel,
} = useValues(MLInferenceLogic);
- if (!selectedMLModel || configuration.existingPipeline) return null;
- const modelType = getMLType(getMlModelTypesForModelConfig(selectedMLModel));
+ if (!selectedModel || configuration.existingPipeline) return null;
+ const modelType = getMLType(selectedModel.types);
switch (modelType) {
case SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION:
return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts
index e12366f42f3ef..ae3cc237c67a5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts
@@ -6,16 +6,16 @@
*/
import { LogicMounter } from '../../../../../__mocks__/kea_logic';
-import { nerModel, textExpansionModel } from '../../../../__mocks__/ml_models.mock';
import { HttpResponse } from '@kbn/core/public';
-import { ErrorResponse } from '../../../../../../../common/types/api';
+import { ErrorResponse, Status } from '../../../../../../../common/types/api';
+import { MlModel, MlModelDeploymentState } from '../../../../../../../common/types/ml';
import { TrainedModelState } from '../../../../../../../common/types/pipelines';
import { GetDocumentsApiLogic } from '../../../../api/documents/get_document_logic';
import { MappingsApiLogic } from '../../../../api/mappings/mappings_logic';
-import { MLModelsApiLogic } from '../../../../api/ml_models/ml_models_logic';
+import { CachedFetchModelsApiLogic } from '../../../../api/ml_models/cached_fetch_models_api_logic';
import { StartTextExpansionModelApiLogic } from '../../../../api/ml_models/text_expansion/start_text_expansion_model_api_logic';
import { AttachMlInferencePipelineApiLogic } from '../../../../api/pipelines/attach_ml_inference_pipeline';
import { CreateMlInferencePipelineApiLogic } from '../../../../api/pipelines/create_ml_inference_pipeline';
@@ -50,6 +50,7 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = {
index: null,
isConfigureStepValid: false,
isLoading: true,
+ isModelsInitialLoading: false,
isPipelineDataValid: false,
isTextExpansionModelSelected: false,
mappingData: undefined,
@@ -57,17 +58,38 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = {
mlInferencePipeline: undefined,
mlInferencePipelineProcessors: undefined,
mlInferencePipelinesData: undefined,
- mlModelsData: null,
- mlModelsStatus: 0,
- selectedMLModel: null,
+ modelsData: undefined,
+ modelsStatus: 0,
+ selectableModels: [],
+ selectedModel: undefined,
sourceFields: undefined,
- supportedMLModels: [],
};
+const MODELS: MlModel[] = [
+ {
+ modelId: 'model_1',
+ type: 'ner',
+ title: 'Model 1',
+ description: 'Model 1 description',
+ licenseType: 'elastic',
+ modelDetailsPageUrl: 'https://my-model.ai',
+ deploymentState: MlModelDeploymentState.NotDeployed,
+ startTime: 0,
+ targetAllocationCount: 0,
+ nodeAllocationCount: 0,
+ threadsPerAllocation: 0,
+ isPlaceholder: false,
+ hasStats: false,
+ types: ['pytorch', 'ner'],
+ inputFieldNames: ['title'],
+ version: '1',
+ },
+];
+
describe('MlInferenceLogic', () => {
const { mount } = new LogicMounter(MLInferenceLogic);
const { mount: mountMappingApiLogic } = new LogicMounter(MappingsApiLogic);
- const { mount: mountMLModelsApiLogic } = new LogicMounter(MLModelsApiLogic);
+ const { mount: mountCachedFetchModelsApiLogic } = new LogicMounter(CachedFetchModelsApiLogic);
const { mount: mountSimulateExistingMlInterfacePipelineApiLogic } = new LogicMounter(
SimulateExistingMlInterfacePipelineApiLogic
);
@@ -92,7 +114,7 @@ describe('MlInferenceLogic', () => {
beforeEach(() => {
jest.clearAllMocks();
mountMappingApiLogic();
- mountMLModelsApiLogic();
+ mountCachedFetchModelsApiLogic();
mountFetchMlInferencePipelineProcessorsApiLogic();
mountFetchMlInferencePipelinesApiLogic();
mountSimulateExistingMlInterfacePipelineApiLogic();
@@ -105,7 +127,13 @@ describe('MlInferenceLogic', () => {
});
it('has expected default values', () => {
- expect(MLInferenceLogic.values).toEqual(DEFAULT_VALUES);
+ CachedFetchModelsApiLogic.actions.apiSuccess(MODELS);
+ expect(MLInferenceLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ modelsData: MODELS, // Populated by afterMount hook
+ modelsStatus: Status.SUCCESS,
+ selectableModels: MODELS,
+ });
});
describe('actions', () => {
@@ -184,6 +212,7 @@ describe('MlInferenceLogic', () => {
describe('selectors', () => {
describe('existingInferencePipelines', () => {
beforeEach(() => {
+ CachedFetchModelsApiLogic.actions.apiSuccess(MODELS);
MappingsApiLogic.actions.apiSuccess({
mappings: {
properties: {
@@ -206,7 +235,7 @@ describe('MlInferenceLogic', () => {
field_map: {
body: 'text_field',
},
- model_id: 'test-model',
+ model_id: MODELS[0].modelId,
target_field: 'ml.inference.test-field',
},
},
@@ -218,8 +247,8 @@ describe('MlInferenceLogic', () => {
expect(MLInferenceLogic.values.existingInferencePipelines).toEqual([
{
disabled: false,
- modelId: 'test-model',
- modelType: '',
+ modelId: MODELS[0].modelId,
+ modelType: 'ner',
pipelineName: 'unit-test',
sourceFields: ['body'],
indexFields: ['body'],
@@ -235,7 +264,7 @@ describe('MlInferenceLogic', () => {
field_map: {
title: 'text_field', // Does not exist in index
},
- model_id: 'test-model',
+ model_id: MODELS[0].modelId,
target_field: 'ml.inference.title',
},
},
@@ -244,7 +273,7 @@ describe('MlInferenceLogic', () => {
field_map: {
body: 'text_field', // Exists in index
},
- model_id: 'test-model',
+ model_id: MODELS[0].modelId,
target_field: 'ml.inference.body',
},
},
@@ -253,7 +282,7 @@ describe('MlInferenceLogic', () => {
field_map: {
body_content: 'text_field', // Does not exist in index
},
- model_id: 'test-model',
+ model_id: MODELS[0].modelId,
target_field: 'ml.inference.body_content',
},
},
@@ -266,8 +295,8 @@ describe('MlInferenceLogic', () => {
{
disabled: true,
disabledReason: expect.stringContaining('title, body_content'),
- modelId: 'test-model',
- modelType: '',
+ modelId: MODELS[0].modelId,
+ modelType: 'ner',
pipelineName: 'unit-test',
sourceFields: ['title', 'body', 'body_content'],
indexFields: ['body'],
@@ -306,7 +335,7 @@ describe('MlInferenceLogic', () => {
it('filters pipeline if pipeline already attached', () => {
FetchMlInferencePipelineProcessorsApiLogic.actions.apiSuccess([
{
- modelId: 'test-model',
+ modelId: MODELS[0].modelId,
modelState: TrainedModelState.Started,
pipelineName: 'unit-test',
pipelineReferences: ['test@ml-inference'],
@@ -321,7 +350,7 @@ describe('MlInferenceLogic', () => {
field_map: {
body: 'text_field',
},
- model_id: 'test-model',
+ model_id: MODELS[0].modelId,
target_field: 'ml.inference.test-field',
},
},
@@ -333,165 +362,6 @@ describe('MlInferenceLogic', () => {
expect(MLInferenceLogic.values.existingInferencePipelines).toEqual([]);
});
});
- describe('mlInferencePipeline', () => {
- it('returns undefined when configuration is invalid', () => {
- MLInferenceLogic.actions.setInferencePipelineConfiguration({
- modelID: '',
- pipelineName: '', // Invalid
- fieldMappings: [], // Invalid
- targetField: '',
- });
-
- expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined();
- });
- it('generates inference pipeline', () => {
- MLModelsApiLogic.actions.apiSuccess([nerModel]);
- MLInferenceLogic.actions.setInferencePipelineConfiguration({
- modelID: nerModel.model_id,
- pipelineName: 'unit-test',
- fieldMappings: [
- {
- sourceField: 'body',
- targetField: 'ml.inference.body',
- },
- ],
- targetField: '',
- });
-
- expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined();
- });
- it('returns undefined when existing pipeline not yet selected', () => {
- MLInferenceLogic.actions.setInferencePipelineConfiguration({
- existingPipeline: true,
- modelID: '',
- pipelineName: '',
- fieldMappings: [],
- targetField: '',
- });
- expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined();
- });
- it('return existing pipeline when selected', () => {
- const existingPipeline = {
- description: 'this is a test',
- processors: [],
- version: 1,
- };
- FetchMlInferencePipelinesApiLogic.actions.apiSuccess({
- 'unit-test': existingPipeline,
- });
- MLInferenceLogic.actions.setInferencePipelineConfiguration({
- existingPipeline: true,
- modelID: '',
- pipelineName: 'unit-test',
- fieldMappings: [
- {
- sourceField: 'body',
- targetField: 'ml.inference.body',
- },
- ],
- targetField: '',
- });
- expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined();
- expect(MLInferenceLogic.values.mlInferencePipeline).toEqual(existingPipeline);
- });
- });
- describe('supportedMLModels', () => {
- it('filters unsupported ML models', () => {
- MLModelsApiLogic.actions.apiSuccess([
- {
- inference_config: {
- ner: {},
- },
- input: {
- field_names: ['text_field'],
- },
- model_id: 'ner-mocked-model',
- model_type: 'pytorch',
- tags: [],
- version: '1',
- },
- {
- inference_config: {
- some_unsupported_task_type: {},
- },
- input: {
- field_names: ['text_field'],
- },
- model_id: 'unsupported-mocked-model',
- model_type: 'pytorch',
- tags: [],
- version: '1',
- },
- ]);
-
- expect(MLInferenceLogic.values.supportedMLModels).toEqual([
- expect.objectContaining({
- inference_config: {
- ner: {},
- },
- }),
- ]);
- });
-
- it('promotes text_expansion ML models and sorts others by ID', () => {
- MLModelsApiLogic.actions.apiSuccess([
- {
- inference_config: {
- ner: {},
- },
- input: {
- field_names: ['text_field'],
- },
- model_id: 'ner-mocked-model',
- model_type: 'pytorch',
- tags: [],
- version: '1',
- },
- {
- inference_config: {
- text_expansion: {},
- },
- input: {
- field_names: ['text_field'],
- },
- model_id: 'text-expansion-mocked-model',
- model_type: 'pytorch',
- tags: [],
- version: '1',
- },
- {
- inference_config: {
- text_embedding: {},
- },
- input: {
- field_names: ['text_field'],
- },
- model_id: 'text-embedding-mocked-model',
- model_type: 'pytorch',
- tags: [],
- version: '1',
- },
- ]);
-
- expect(MLInferenceLogic.values.supportedMLModels).toEqual([
- expect.objectContaining({
- inference_config: {
- text_expansion: {},
- },
- }),
- expect.objectContaining({
- inference_config: {
- ner: {},
- },
- }),
- expect.objectContaining({
- inference_config: {
- text_embedding: {},
- },
- }),
- ]);
- });
- });
describe('formErrors', () => {
it('has errors when configuration is empty', () => {
expect(MLInferenceLogic.values.formErrors).toEqual({
@@ -570,6 +440,75 @@ describe('MlInferenceLogic', () => {
});
});
});
+ describe('mlInferencePipeline', () => {
+ it('returns undefined when configuration is invalid', () => {
+ MLInferenceLogic.actions.setInferencePipelineConfiguration({
+ modelID: '',
+ pipelineName: '', // Invalid
+ fieldMappings: [], // Invalid
+ targetField: '',
+ });
+
+ expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined();
+ });
+ it('generates inference pipeline', () => {
+ CachedFetchModelsApiLogic.actions.apiSuccess(MODELS);
+ MLInferenceLogic.actions.setInferencePipelineConfiguration({
+ modelID: MODELS[0].modelId,
+ pipelineName: 'unit-test',
+ fieldMappings: [
+ {
+ sourceField: 'body',
+ targetField: 'ml.inference.body',
+ },
+ ],
+ targetField: '',
+ });
+
+ expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined();
+ });
+ it('returns undefined when existing pipeline not yet selected', () => {
+ MLInferenceLogic.actions.setInferencePipelineConfiguration({
+ existingPipeline: true,
+ modelID: '',
+ pipelineName: '',
+ fieldMappings: [],
+ targetField: '',
+ });
+ expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined();
+ });
+ it('return existing pipeline when selected', () => {
+ const existingPipeline = {
+ description: 'this is a test',
+ processors: [],
+ version: 1,
+ };
+ FetchMlInferencePipelinesApiLogic.actions.apiSuccess({
+ 'unit-test': existingPipeline,
+ });
+ MLInferenceLogic.actions.setInferencePipelineConfiguration({
+ existingPipeline: true,
+ modelID: '',
+ pipelineName: 'unit-test',
+ fieldMappings: [
+ {
+ sourceField: 'body',
+ targetField: 'ml.inference.body',
+ },
+ ],
+ targetField: '',
+ });
+ expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined();
+ expect(MLInferenceLogic.values.mlInferencePipeline).toEqual(existingPipeline);
+ });
+ });
+ describe('selectableModels', () => {
+ it('makes fetch models request', () => {
+ MLInferenceLogic.actions.fetchModelsApiSuccess(MODELS);
+
+ expect(MLInferenceLogic.values.selectableModels).toBe(MODELS);
+ });
+ });
});
describe('listeners', () => {
@@ -615,14 +554,14 @@ describe('MlInferenceLogic', () => {
...mockModelConfiguration,
configuration: {
...mockModelConfiguration.configuration,
- modelID: textExpansionModel.model_id,
+ modelID: MODELS[0].modelId,
fieldMappings: [],
},
},
});
jest.spyOn(MLInferenceLogic.actions, 'makeCreatePipelineRequest');
- MLModelsApiLogic.actions.apiSuccess([textExpansionModel]);
+ CachedFetchModelsApiLogic.actions.apiSuccess(MODELS);
MLInferenceLogic.actions.selectFields(['my_source_field1', 'my_source_field2']);
MLInferenceLogic.actions.addSelectedFieldsToMapping(true);
MLInferenceLogic.actions.createPipeline();
@@ -630,7 +569,7 @@ describe('MlInferenceLogic', () => {
expect(MLInferenceLogic.actions.makeCreatePipelineRequest).toHaveBeenCalledWith({
indexName: mockModelConfiguration.indexName,
inferenceConfig: undefined,
- modelId: textExpansionModel.model_id,
+ modelId: MODELS[0].modelId,
fieldMappings: [
{
sourceField: 'my_source_field1',
@@ -648,13 +587,13 @@ describe('MlInferenceLogic', () => {
});
describe('startTextExpansionModelSuccess', () => {
it('fetches ml models', () => {
- jest.spyOn(MLInferenceLogic.actions, 'makeMLModelsRequest');
+ jest.spyOn(MLInferenceLogic.actions, 'startPollingModels');
StartTextExpansionModelApiLogic.actions.apiSuccess({
deploymentState: 'started',
modelId: 'foo',
});
- expect(MLInferenceLogic.actions.makeMLModelsRequest).toHaveBeenCalledWith(undefined);
+ expect(MLInferenceLogic.actions.startPollingModels).toHaveBeenCalled();
});
});
describe('onAddInferencePipelineStepChange', () => {
@@ -673,12 +612,12 @@ describe('MlInferenceLogic', () => {
existingPipeline: false,
});
jest.spyOn(MLInferenceLogic.actions, 'fetchPipelineByName');
- jest.spyOn(MLInferenceLogic.actions, 'makeMLModelsRequest');
+ jest.spyOn(MLInferenceLogic.actions, 'startPollingModels');
MLInferenceLogic.actions.onAddInferencePipelineStepChange(AddInferencePipelineSteps.Fields);
expect(MLInferenceLogic.actions.fetchPipelineByName).toHaveBeenCalledWith({
pipelineName: 'ml-inference-unit-test-pipeline',
});
- expect(MLInferenceLogic.actions.makeMLModelsRequest).toHaveBeenCalledWith(undefined);
+ expect(MLInferenceLogic.actions.startPollingModels).toHaveBeenCalled();
});
it('does not trigger pipeline and model fetch existing pipeline is selected', () => {
MLInferenceLogic.actions.setInferencePipelineConfiguration({
@@ -688,10 +627,10 @@ describe('MlInferenceLogic', () => {
existingPipeline: true,
});
jest.spyOn(MLInferenceLogic.actions, 'fetchPipelineByName');
- jest.spyOn(MLInferenceLogic.actions, 'makeMLModelsRequest');
+ jest.spyOn(MLInferenceLogic.actions, 'startPollingModels');
MLInferenceLogic.actions.onAddInferencePipelineStepChange(AddInferencePipelineSteps.Fields);
expect(MLInferenceLogic.actions.fetchPipelineByName).not.toHaveBeenCalled();
- expect(MLInferenceLogic.actions.makeMLModelsRequest).not.toHaveBeenCalled();
+ expect(MLInferenceLogic.actions.startPollingModels).not.toHaveBeenCalled();
});
});
describe('fetchPipelineSuccess', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts
index bdcf23d71a743..3383a8772f3ce 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts
@@ -14,11 +14,11 @@ import {
formatPipelineName,
generateMlInferencePipelineBody,
getMlInferencePrefixedFieldName,
- getMlModelTypesForModelConfig,
ML_INFERENCE_PREFIX,
parseMlInferenceParametersFromPipeline,
} from '../../../../../../../common/ml_inference_pipeline';
import { Status } from '../../../../../../../common/types/api';
+import { MlModel } from '../../../../../../../common/types/ml';
import { MlInferencePipeline } from '../../../../../../../common/types/pipelines';
import { Actions } from '../../../../../shared/api_logic/create_api_logic';
@@ -34,10 +34,10 @@ import {
MappingsApiLogic,
} from '../../../../api/mappings/mappings_logic';
import {
- TrainedModel,
- TrainedModelsApiLogicActions,
- TrainedModelsApiLogic,
-} from '../../../../api/ml_models/ml_trained_models_logic';
+ CachedFetchModelsApiLogic,
+ CachedFetchModlesApiLogicActions,
+ FetchModelsApiResponse,
+} from '../../../../api/ml_models/cached_fetch_models_api_logic';
import {
StartTextExpansionModelApiLogic,
StartTextExpansionModelApiLogicActions,
@@ -68,12 +68,7 @@ import {
} from '../../../../api/pipelines/fetch_pipeline';
import { isConnectorIndex } from '../../../../utils/indices';
-import {
- getMLType,
- isSupportedMLModel,
- sortModels,
- sortSourceFields,
-} from '../../../shared/ml_inference/utils';
+import { getMLType, sortSourceFields } from '../../../shared/ml_inference/utils';
import { PipelinesLogic } from '../pipelines_logic';
import {
@@ -143,6 +138,7 @@ export interface MLInferenceProcessorsActions {
CreateMlInferencePipelineResponse
>['apiSuccess'];
createPipeline: () => void;
+ fetchModelsApiSuccess: CachedFetchModlesApiLogicActions['apiSuccess'];
fetchPipelineByName: FetchPipelineApiLogicActions['makeRequest'];
fetchPipelineSuccess: FetchPipelineApiLogicActions['apiSuccess'];
makeAttachPipelineRequest: Actions<
@@ -153,7 +149,6 @@ export interface MLInferenceProcessorsActions {
CreateMlInferencePipelineApiLogicArgs,
CreateMlInferencePipelineResponse
>['makeRequest'];
- makeMLModelsRequest: TrainedModelsApiLogicActions['makeRequest'];
makeMappingRequest: Actions['makeRequest'];
makeMlInferencePipelinesRequest: Actions<
FetchMlInferencePipelinesArgs,
@@ -164,7 +159,6 @@ export interface MLInferenceProcessorsActions {
FetchMlInferencePipelinesArgs,
FetchMlInferencePipelinesResponse
>['apiSuccess'];
- mlModelsApiError: TrainedModelsApiLogicActions['apiError'];
onAddInferencePipelineStepChange: (step: AddInferencePipelineSteps) => {
step: AddInferencePipelineSteps;
};
@@ -181,6 +175,7 @@ export interface MLInferenceProcessorsActions {
configuration: InferencePipelineConfiguration;
};
setTargetField: (targetFieldName: string) => { targetFieldName: string };
+ startPollingModels: CachedFetchModlesApiLogicActions['startPolling'];
startTextExpansionModelSuccess: StartTextExpansionModelApiLogicActions['apiSuccess'];
}
@@ -200,6 +195,7 @@ export interface MLInferenceProcessorsValues {
index: CachedFetchIndexApiLogicValues['indexData'];
isConfigureStepValid: boolean;
isLoading: boolean;
+ isModelsInitialLoading: boolean;
isPipelineDataValid: boolean;
isTextExpansionModelSelected: boolean;
mappingData: typeof MappingsApiLogic.values.data;
@@ -207,11 +203,11 @@ export interface MLInferenceProcessorsValues {
mlInferencePipeline: MlInferencePipeline | undefined;
mlInferencePipelineProcessors: FetchMlInferencePipelineProcessorsResponse | undefined;
mlInferencePipelinesData: FetchMlInferencePipelinesResponse | undefined;
- mlModelsData: TrainedModel[] | null;
- mlModelsStatus: Status;
- selectedMLModel: TrainedModel | null;
+ modelsData: FetchModelsApiResponse | undefined;
+ modelsStatus: Status;
+ selectableModels: MlModel[];
+ selectedModel: MlModel | undefined;
sourceFields: string[] | undefined;
- supportedMLModels: TrainedModel[];
}
export const MLInferenceLogic = kea<
@@ -238,6 +234,8 @@ export const MLInferenceLogic = kea<
},
connect: {
actions: [
+ CachedFetchModelsApiLogic,
+ ['apiSuccess as fetchModelsApiSuccess', 'startPolling as startPollingModels'],
FetchMlInferencePipelinesApiLogic,
[
'makeRequest as makeMlInferencePipelinesRequest',
@@ -245,8 +243,6 @@ export const MLInferenceLogic = kea<
],
MappingsApiLogic,
['makeRequest as makeMappingRequest', 'apiError as mappingsApiError'],
- TrainedModelsApiLogic,
- ['makeRequest as makeMLModelsRequest', 'apiError as mlModelsApiError'],
CreateMlInferencePipelineApiLogic,
[
'apiError as createApiError',
@@ -260,7 +256,7 @@ export const MLInferenceLogic = kea<
'makeRequest as makeAttachPipelineRequest',
],
PipelinesLogic,
- ['closeAddMlInferencePipelineModal as closeAddMlInferencePipelineModal'],
+ ['closeAddMlInferencePipelineModal'],
StartTextExpansionModelApiLogic,
['apiSuccess as startTextExpansionModelSuccess'],
FetchPipelineApiLogic,
@@ -271,21 +267,20 @@ export const MLInferenceLogic = kea<
],
],
values: [
+ CachedFetchModelsApiLogic,
+ ['modelsData', 'status as modelsStatus', 'isInitialLoading as isModelsInitialLoading'],
CachedFetchIndexApiLogic,
['indexData as index'],
FetchMlInferencePipelinesApiLogic,
['data as mlInferencePipelinesData'],
MappingsApiLogic,
['data as mappingData', 'status as mappingStatus'],
- TrainedModelsApiLogic,
- ['data as mlModelsData', 'status as mlModelsStatus'],
FetchMlInferencePipelineProcessorsApiLogic,
['data as mlInferencePipelineProcessors'],
FetchPipelineApiLogic,
['data as existingPipeline'],
],
},
- events: {},
listeners: ({ values, actions }) => ({
attachPipeline: () => {
const {
@@ -340,11 +335,6 @@ export const MLInferenceLogic = kea<
targetField: '',
});
},
- setIndexName: ({ indexName }) => {
- actions.makeMlInferencePipelinesRequest(undefined);
- actions.makeMLModelsRequest(undefined);
- actions.makeMappingRequest({ indexName });
- },
mlInferencePipelinesSuccess: (data) => {
if (
(data?.length ?? 0) === 0 &&
@@ -359,7 +349,7 @@ export const MLInferenceLogic = kea<
},
startTextExpansionModelSuccess: () => {
// Refresh ML models list when the text expansion model is started
- actions.makeMLModelsRequest(undefined);
+ actions.startPollingModels();
},
onAddInferencePipelineStepChange: ({ step }) => {
const {
@@ -377,12 +367,12 @@ export const MLInferenceLogic = kea<
// back to the Configuration step if we find a pipeline with the same name
// Re-fetch ML model list to include those that were deployed in this step
- actions.makeMLModelsRequest(undefined);
+ actions.startPollingModels();
}
actions.setAddInferencePipelineStep(step);
},
fetchPipelineSuccess: () => {
- // We found a pipeline with the name go back to configuration step
+ // We found a pipeline with the name, go back to configuration step
actions.setAddInferencePipelineStep(AddInferencePipelineSteps.Configuration);
},
}),
@@ -509,30 +499,28 @@ export const MLInferenceLogic = kea<
},
],
isLoading: [
- () => [selectors.mlModelsStatus, selectors.mappingStatus],
- (mlModelsStatus, mappingStatus) =>
- !API_REQUEST_COMPLETE_STATUSES.includes(mlModelsStatus) ||
- !API_REQUEST_COMPLETE_STATUSES.includes(mappingStatus),
+ () => [selectors.mappingStatus],
+ (mappingStatus: Status) => !API_REQUEST_COMPLETE_STATUSES.includes(mappingStatus),
],
isPipelineDataValid: [
() => [selectors.formErrors],
(errors: AddInferencePipelineFormErrors) => Object.keys(errors).length === 0,
],
isTextExpansionModelSelected: [
- () => [selectors.selectedMLModel],
- (model: TrainedModel | null) => !!model?.inference_config?.text_expansion,
+ () => [selectors.selectedModel],
+ (model: MlModel | null) => model?.type === 'text_expansion',
],
mlInferencePipeline: [
() => [
selectors.isPipelineDataValid,
selectors.addInferencePipelineModal,
- selectors.mlModelsData,
+ selectors.modelsData,
selectors.mlInferencePipelinesData,
],
(
isPipelineDataValid: MLInferenceProcessorsValues['isPipelineDataValid'],
{ configuration }: MLInferenceProcessorsValues['addInferencePipelineModal'],
- models: MLInferenceProcessorsValues['mlModelsData'],
+ models: MLInferenceProcessorsValues['modelsData'],
mlInferencePipelinesData: MLInferenceProcessorsValues['mlInferencePipelinesData']
) => {
if (configuration.existingPipeline) {
@@ -546,7 +534,7 @@ export const MLInferenceLogic = kea<
return pipeline as MlInferencePipeline;
}
if (!isPipelineDataValid) return undefined;
- const model = models?.find((mlModel) => mlModel.model_id === configuration.modelID);
+ const model = models?.find((mlModel) => mlModel.modelId === configuration.modelID);
if (!model) return undefined;
return generateMlInferencePipelineBody({
@@ -581,23 +569,28 @@ export const MLInferenceLogic = kea<
.sort(sortSourceFields);
},
],
- supportedMLModels: [
- () => [selectors.mlModelsData],
- (mlModelsData: MLInferenceProcessorsValues['mlModelsData']) => {
- return (mlModelsData?.filter(isSupportedMLModel) ?? []).sort(sortModels);
- },
+ selectableModels: [
+ () => [selectors.modelsData],
+ (response: FetchModelsApiResponse) => response ?? [],
+ ],
+ selectedModel: [
+ () => [selectors.selectableModels, selectors.addInferencePipelineModal],
+ (
+ models: MlModel[],
+ addInferencePipelineModal: MLInferenceProcessorsValues['addInferencePipelineModal']
+ ) => models.find((m) => m.modelId === addInferencePipelineModal.configuration.modelID),
],
existingInferencePipelines: [
() => [
selectors.mlInferencePipelinesData,
selectors.sourceFields,
- selectors.supportedMLModels,
+ selectors.selectableModels,
selectors.mlInferencePipelineProcessors,
],
(
mlInferencePipelinesData: MLInferenceProcessorsValues['mlInferencePipelinesData'],
indexFields: MLInferenceProcessorsValues['sourceFields'],
- supportedMLModels: MLInferenceProcessorsValues['supportedMLModels'],
+ selectableModels: MLInferenceProcessorsValues['selectableModels'],
mlInferencePipelineProcessors: MLInferenceProcessorsValues['mlInferencePipelineProcessors']
) => {
if (!mlInferencePipelinesData) {
@@ -619,8 +612,8 @@ export const MLInferenceLogic = kea<
const sourceFields = fieldMappings?.map((m) => m.sourceField) ?? [];
const missingSourceFields = sourceFields.filter((f) => !indexFields?.includes(f)) ?? [];
- const mlModel = supportedMLModels.find((model) => model.model_id === modelId);
- const modelType = mlModel ? getMLType(getMlModelTypesForModelConfig(mlModel)) : '';
+ const mlModel = selectableModels.find((model) => model.modelId === modelId);
+ const modelType = mlModel ? getMLType(mlModel.types) : '';
const disabledReason =
missingSourceFields.length > 0
? EXISTING_PIPELINE_DISABLED_MISSING_SOURCE_FIELDS(missingSourceFields.join(', '))
@@ -641,18 +634,5 @@ export const MLInferenceLogic = kea<
return existingPipelines;
},
],
- selectedMLModel: [
- () => [selectors.supportedMLModels, selectors.addInferencePipelineModal],
- (
- supportedMLModels: MLInferenceProcessorsValues['supportedMLModels'],
- addInferencePipelineModal: MLInferenceProcessorsValues['addInferencePipelineModal']
- ) => {
- return (
- supportedMLModels.find(
- (model) => model.model_id === addInferencePipelineModal.configuration.modelID
- ) ?? null
- );
- },
- ],
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx
index c8a970751643a..b08b4697e6cfc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx
@@ -52,6 +52,9 @@ const DEFAULT_MODEL: MlModel = {
threadsPerAllocation: 0,
isPlaceholder: false,
hasStats: false,
+ types: ['pytorch', 'ner'],
+ inputFieldNames: ['title'],
+ version: '1',
};
const MOCK_ACTIONS = {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts
index b0c26aaf8be8c..8af7d59a1ceec 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts
@@ -8,7 +8,7 @@
import { LogicMounter } from '../../../../../__mocks__/kea_logic';
import { HttpError } from '../../../../../../../common/types/api';
-import { MlModel, MlModelDeploymentState } from '../../../../../../../common/types/ml';
+import { MlModelDeploymentState } from '../../../../../../../common/types/ml';
import { CachedFetchModelsApiLogic } from '../../../../api/ml_models/cached_fetch_models_api_logic';
import {
CreateModelApiLogic,
@@ -22,31 +22,15 @@ const CREATE_MODEL_API_RESPONSE: CreateModelResponse = {
modelId: 'model_1',
deploymentState: MlModelDeploymentState.NotDeployed,
};
-const FETCH_MODELS_API_DATA_RESPONSE: MlModel[] = [
- {
- modelId: 'model_1',
- title: 'Model 1',
- type: 'ner',
- deploymentState: MlModelDeploymentState.NotDeployed,
- startTime: 0,
- targetAllocationCount: 0,
- nodeAllocationCount: 0,
- threadsPerAllocation: 0,
- isPlaceholder: false,
- hasStats: false,
- },
-];
describe('ModelSelectLogic', () => {
const { mount } = new LogicMounter(ModelSelectLogic);
const { mount: mountCreateModelApiLogic } = new LogicMounter(CreateModelApiLogic);
- const { mount: mountCachedFetchModelsApiLogic } = new LogicMounter(CachedFetchModelsApiLogic);
const { mount: mountStartModelApiLogic } = new LogicMounter(StartModelApiLogic);
beforeEach(() => {
jest.clearAllMocks();
mountCreateModelApiLogic();
- mountCachedFetchModelsApiLogic();
mountStartModelApiLogic();
mount();
});
@@ -82,16 +66,6 @@ describe('ModelSelectLogic', () => {
});
});
- describe('fetchModels', () => {
- it('makes fetch models request', () => {
- jest.spyOn(ModelSelectLogic.actions, 'fetchModelsMakeRequest');
-
- ModelSelectLogic.actions.fetchModels();
-
- expect(ModelSelectLogic.actions.fetchModelsMakeRequest).toHaveBeenCalled();
- });
- });
-
describe('startModel', () => {
it('makes start model request', () => {
const modelId = 'model_1';
@@ -150,14 +124,6 @@ describe('ModelSelectLogic', () => {
});
});
- describe('selectableModels', () => {
- it('gets models data from API response', () => {
- CachedFetchModelsApiLogic.actions.apiSuccess(FETCH_MODELS_API_DATA_RESPONSE);
-
- expect(ModelSelectLogic.values.selectableModels).toEqual(FETCH_MODELS_API_DATA_RESPONSE);
- });
- });
-
describe('isLoading', () => {
it('is set to true if the fetch API is loading the first time', () => {
CachedFetchModelsApiLogic.actions.apiReset();
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts
index 4074ffac92f6b..09fd2e0ae8f54 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts
@@ -10,15 +10,10 @@ import { kea, MakeLogicType } from 'kea';
import { HttpError, Status } from '../../../../../../../common/types/api';
import { MlModel } from '../../../../../../../common/types/ml';
import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors';
-import {
- CachedFetchModelsApiLogic,
- CachedFetchModlesApiLogicActions,
-} from '../../../../api/ml_models/cached_fetch_models_api_logic';
import {
CreateModelApiLogic,
CreateModelApiLogicActions,
} from '../../../../api/ml_models/create_model_api_logic';
-import { FetchModelsApiResponse } from '../../../../api/ml_models/fetch_models_api_logic';
import {
StartModelApiLogic,
StartModelApiLogicActions,
@@ -37,17 +32,13 @@ export interface ModelSelectActions {
createModelError: CreateModelApiLogicActions['apiError'];
createModelMakeRequest: CreateModelApiLogicActions['makeRequest'];
createModelSuccess: CreateModelApiLogicActions['apiSuccess'];
- fetchModels: () => void;
- fetchModelsError: CachedFetchModlesApiLogicActions['apiError'];
- fetchModelsMakeRequest: CachedFetchModlesApiLogicActions['makeRequest'];
- fetchModelsSuccess: CachedFetchModlesApiLogicActions['apiSuccess'];
setInferencePipelineConfiguration: MLInferenceProcessorsActions['setInferencePipelineConfiguration'];
setInferencePipelineConfigurationFromMLInferenceLogic: MLInferenceProcessorsActions['setInferencePipelineConfiguration'];
startModel: (modelId: string) => { modelId: string };
startModelError: CreateModelApiLogicActions['apiError'];
startModelMakeRequest: StartModelApiLogicActions['makeRequest'];
startModelSuccess: StartModelApiLogicActions['apiSuccess'];
- startPollingModels: CachedFetchModlesApiLogicActions['startPolling'];
+ startPollingModels: MLInferenceProcessorsActions['startPollingModels'];
}
export interface ModelSelectValues {
@@ -59,12 +50,12 @@ export interface ModelSelectValues {
ingestionMethod: string;
ingestionMethodFromIndexViewLogic: string;
isLoading: boolean;
- isInitialLoading: boolean;
+ isModelsInitialLoadingFromMLInferenceLogic: boolean;
modelStateChangeError: string | undefined;
- modelsData: FetchModelsApiResponse | undefined;
- modelsStatus: Status;
selectableModels: MlModel[];
+ selectableModelsFromMLInferenceLogic: MlModel[];
selectedModel: MlModel | undefined;
+ selectedModelFromMLInferenceLogic: MlModel | undefined;
startModelError: HttpError | undefined;
startModelStatus: Status;
}
@@ -72,19 +63,11 @@ export interface ModelSelectValues {
export const ModelSelectLogic = kea>({
actions: {
createModel: (modelId: string) => ({ modelId }),
- fetchModels: true,
setInferencePipelineConfiguration: (configuration) => ({ configuration }),
startModel: (modelId: string) => ({ modelId }),
},
connect: {
actions: [
- CachedFetchModelsApiLogic,
- [
- 'makeRequest as fetchModelsMakeRequest',
- 'apiSuccess as fetchModelsSuccess',
- 'apiError as fetchModelsError',
- 'startPolling as startPollingModels',
- ],
CreateModelApiLogic,
[
'makeRequest as createModelMakeRequest',
@@ -93,8 +76,9 @@ export const ModelSelectLogic = kea ({
- afterMount: () => {
- actions.startPollingModels();
- },
- }),
listeners: ({ actions }) => ({
createModel: ({ modelId }) => {
actions.createModelMakeRequest({ modelId });
@@ -130,9 +112,6 @@ export const ModelSelectLogic = kea {
- actions.fetchModelsMakeRequest({});
- },
setInferencePipelineConfiguration: ({ configuration }) => {
actions.setInferencePipelineConfigurationFromMLInferenceLogic(configuration);
},
@@ -167,16 +146,16 @@ export const ModelSelectLogic = kea [selectors.modelsData],
- (response: FetchModelsApiResponse) => response ?? [],
+ () => [selectors.selectableModelsFromMLInferenceLogic],
+ (selectableModels) => selectableModels, // Pass-through
],
selectedModel: [
- () => [selectors.selectableModels, selectors.addInferencePipelineModal],
- (
- models: MlModel[],
- addInferencePipelineModal: MLInferenceProcessorsValues['addInferencePipelineModal']
- ) => models.find((m) => m.modelId === addInferencePipelineModal.configuration.modelID),
+ () => [selectors.selectedModelFromMLInferenceLogic],
+ (selectedModel) => selectedModel, // Pass-through
+ ],
+ isLoading: [
+ () => [selectors.isModelsInitialLoadingFromMLInferenceLogic],
+ (isModelsInitialLoading) => isModelsInitialLoading, // Pass-through
],
- isLoading: [() => [selectors.isInitialLoading], (isInitialLoading) => isInitialLoading],
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx
index bcf4eb8342db1..6c4f1f4bbabb8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx
@@ -34,6 +34,9 @@ const DEFAULT_PROPS: EuiSelectableOption = {
threadsPerAllocation: 0,
isPlaceholder: false,
hasStats: false,
+ types: ['pytorch', 'ner'],
+ inputFieldNames: ['title'],
+ version: '1',
};
describe('ModelSelectOption', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
deleted file mode 100644
index 4670b00e93927..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { EuiEmptyPrompt, EuiImage, EuiLink, EuiText, useEuiTheme } from '@elastic/eui';
-
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-
-import noMlModelsGraphicDark from '../../../../../../assets/images/no_ml_models_dark.svg';
-import noMlModelsGraphicLight from '../../../../../../assets/images/no_ml_models_light.svg';
-
-import { docLinks } from '../../../../../shared/doc_links';
-
-export const NoModelsPanel: React.FC = () => {
- const { colorMode } = useEuiTheme();
-
- return (
-
-
-
-
-
- {i18n.translate(
- 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link',
- { defaultMessage: 'Learn how to add a trained model' }
- )}
-
- ),
- }}
- />
-
-
- >
- }
- />
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts
index 4ddf5b1c4b77a..1e4eb17744517 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts
@@ -4,89 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { nerModel, textClassificationModel } from '../../../__mocks__/ml_models.mock';
-import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models';
-
-import {
- getMLType,
- getModelDisplayTitle,
- isSupportedMLModel,
- sortSourceFields,
- NLP_CONFIG_KEYS,
-} from './utils';
+import { getMLType, getModelDisplayTitle, sortSourceFields, NLP_CONFIG_KEYS } from './utils';
describe('ml inference utils', () => {
- describe('isSupportedMLModel', () => {
- const makeFakeModel = (
- config: Partial
- ): TrainedModelConfigResponse => {
- const { inference_config: _throwAway, ...base } = nerModel;
- return {
- inference_config: {},
- ...base,
- ...config,
- };
- };
- it('returns true for expected models', () => {
- const models: TrainedModelConfigResponse[] = [
- nerModel,
- textClassificationModel,
- makeFakeModel({
- inference_config: {
- text_embedding: {},
- },
- model_id: 'mock-text_embedding',
- }),
- makeFakeModel({
- inference_config: {
- zero_shot_classification: {
- classification_labels: [],
- },
- },
- model_id: 'mock-zero_shot_classification',
- }),
- makeFakeModel({
- inference_config: {
- question_answering: {},
- },
- model_id: 'mock-question_answering',
- }),
- makeFakeModel({
- inference_config: {
- fill_mask: {},
- },
- model_id: 'mock-fill_mask',
- }),
- makeFakeModel({
- inference_config: {
- classification: {},
- },
- model_id: 'lang_ident_model_1',
- model_type: 'lang_ident',
- }),
- ];
-
- for (const model of models) {
- expect(isSupportedMLModel(model)).toBe(true);
- }
- });
-
- it('returns false for unexpected models', () => {
- const models: TrainedModelConfigResponse[] = [
- makeFakeModel({}),
- makeFakeModel({
- inference_config: {
- fakething: {},
- },
- }),
- ];
-
- for (const model of models) {
- expect(isSupportedMLModel(model)).toBe(false);
- }
- });
- });
describe('sortSourceFields', () => {
it('promotes fields', () => {
let fields: string[] = ['id', 'body', 'url'];
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts
index 3d97f52c659c1..88d273fcef135 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts
@@ -6,12 +6,9 @@
*/
import { i18n } from '@kbn/i18n';
-import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models';
import { TRAINED_MODEL_TYPE, SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils';
-import { TrainedModel } from '../../../api/ml_models/ml_trained_models_logic';
-
export const NLP_CONFIG_KEYS: string[] = Object.values(SUPPORTED_PYTORCH_TASKS);
export const RECOMMENDED_FIELDS = ['body', 'body_content', 'title'];
@@ -51,13 +48,6 @@ export const NLP_DISPLAY_TITLES: Record = {
),
};
-export const isSupportedMLModel = (model: TrainedModelConfigResponse): boolean => {
- return (
- Object.keys(model.inference_config || {}).some((key) => NLP_CONFIG_KEYS.includes(key)) ||
- model.model_type === TRAINED_MODEL_TYPE.LANG_IDENT
- );
-};
-
export const sortSourceFields = (a: string, b: string): number => {
const promoteA = RECOMMENDED_FIELDS.includes(a);
const promoteB = RECOMMENDED_FIELDS.includes(b);
@@ -82,16 +72,3 @@ export const getMLType = (modelTypes: string[]): string => {
};
export const getModelDisplayTitle = (type: string): string | undefined => NLP_DISPLAY_TITLES[type];
-
-export const isTextExpansionModel = (model: TrainedModel): boolean =>
- Boolean(model.inference_config?.text_expansion);
-
-/**
- * Sort function for displaying a list of models. Promotes text_expansion models and sorts the rest by model ID.
- */
-export const sortModels = (m1: TrainedModel, m2: TrainedModel) =>
- isTextExpansionModel(m1)
- ? -1
- : isTextExpansionModel(m2)
- ? 1
- : m1.model_id.localeCompare(m2.model_id);
diff --git a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts
index bd02af095fe04..b96bfe6de8e7f 100644
--- a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts
@@ -72,12 +72,18 @@ describe('fetchMlModels', () => {
inference_config: {
text_embedding: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: 'model_1',
inference_config: {
text_classification: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
],
};
@@ -160,24 +166,36 @@ describe('fetchMlModels', () => {
inference_config: {
text_embedding: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: E5_LINUX_OPTIMIZED_MODEL_ID,
inference_config: {
text_embedding: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: ELSER_MODEL_ID,
inference_config: {
text_expansion: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: ELSER_LINUX_OPTIMIZED_MODEL_ID,
inference_config: {
text_expansion: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
],
};
@@ -210,24 +228,36 @@ describe('fetchMlModels', () => {
inference_config: {
text_embedding: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: E5_LINUX_OPTIMIZED_MODEL_ID,
inference_config: {
text_embedding: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: ELSER_MODEL_ID,
inference_config: {
text_expansion: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: ELSER_LINUX_OPTIMIZED_MODEL_ID,
inference_config: {
text_expansion: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
],
};
@@ -265,18 +295,27 @@ describe('fetchMlModels', () => {
inference_config: {
text_expansion: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: E5_MODEL_ID,
inference_config: {
text_embedding: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: 'model_1',
inference_config: {
ner: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
],
};
@@ -337,6 +376,9 @@ describe('fetchMlModels', () => {
inference_config: {
text_expansion: {},
},
+ input: {
+ fields: ['text_field'],
+ },
},
],
};
@@ -385,18 +427,27 @@ describe('fetchMlModels', () => {
inference_config: {
ner: {}, // "Named Entity Recognition"
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: 'model_2',
inference_config: {
text_embedding: {}, // "Dense Vector Text Embedding"
},
+ input: {
+ fields: ['text_field'],
+ },
},
{
model_id: 'model_3',
inference_config: {
text_classification: {}, // "Text Classification"
},
+ input: {
+ fields: ['text_field'],
+ },
},
],
};
diff --git a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts
index c1af4ab69c0bc..c39b6ec146ea1 100644
--- a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts
@@ -6,9 +6,12 @@
*/
import { MlTrainedModelConfig, MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types';
+
import { i18n } from '@kbn/i18n';
import { MlTrainedModels } from '@kbn/ml-plugin/server';
+import { getMlModelTypesForModelConfig } from '../../../common/ml_inference_pipeline';
+
import { MlModelDeploymentState, MlModel } from '../../../common/types/ml';
import {
@@ -109,39 +112,39 @@ export const fetchCompatiblePromotedModelIds = async (trainedModelsProvider: MlT
};
const getModel = (modelConfig: MlTrainedModelConfig, modelStats?: MlTrainedModelStats): MlModel => {
- {
- const modelId = modelConfig.model_id;
- const type = modelConfig.inference_config ? Object.keys(modelConfig.inference_config)[0] : '';
- const model = {
- ...BASE_MODEL,
- modelId,
- type,
- title: getUserFriendlyTitle(modelId, type),
- isPromoted: [
- ELSER_MODEL_ID,
- ELSER_LINUX_OPTIMIZED_MODEL_ID,
- E5_MODEL_ID,
- E5_LINUX_OPTIMIZED_MODEL_ID,
- ].includes(modelId),
- };
-
- // Enrich deployment stats
- if (modelStats && modelStats.deployment_stats) {
- model.hasStats = true;
- model.deploymentState = getDeploymentState(
- modelStats.deployment_stats.allocation_status.state
- );
- model.nodeAllocationCount = modelStats.deployment_stats.allocation_status.allocation_count;
- model.targetAllocationCount =
- modelStats.deployment_stats.allocation_status.target_allocation_count;
- model.threadsPerAllocation = modelStats.deployment_stats.threads_per_allocation;
- model.startTime = modelStats.deployment_stats.start_time;
- } else if (model.modelId === LANG_IDENT_MODEL_ID) {
- model.deploymentState = MlModelDeploymentState.FullyAllocated;
- }
-
- return model;
+ const modelId = modelConfig.model_id;
+ const type = modelConfig.inference_config ? Object.keys(modelConfig.inference_config)[0] : '';
+ const model = {
+ ...BASE_MODEL,
+ modelId,
+ type,
+ title: getUserFriendlyTitle(modelId, type),
+ description: modelConfig.description,
+ types: getMlModelTypesForModelConfig(modelConfig),
+ inputFieldNames: modelConfig.input.field_names,
+ version: modelConfig.version,
+ isPromoted: [
+ ELSER_MODEL_ID,
+ ELSER_LINUX_OPTIMIZED_MODEL_ID,
+ E5_MODEL_ID,
+ E5_LINUX_OPTIMIZED_MODEL_ID,
+ ].includes(modelId),
+ };
+
+ // Enrich deployment stats
+ if (modelStats && modelStats.deployment_stats) {
+ model.hasStats = true;
+ model.deploymentState = getDeploymentState(modelStats.deployment_stats.allocation_status.state);
+ model.nodeAllocationCount = modelStats.deployment_stats.allocation_status.allocation_count;
+ model.targetAllocationCount =
+ modelStats.deployment_stats.allocation_status.target_allocation_count;
+ model.threadsPerAllocation = modelStats.deployment_stats.threads_per_allocation;
+ model.startTime = modelStats.deployment_stats.start_time;
+ } else if (model.modelId === LANG_IDENT_MODEL_ID) {
+ model.deploymentState = MlModelDeploymentState.FullyAllocated;
}
+
+ return model;
};
const enrichModelWithDownloadStatus = async (
diff --git a/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts b/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts
index 19a43059d3a08..d063fd158385a 100644
--- a/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts
@@ -60,6 +60,8 @@ export const BASE_MODEL = {
threadsPerAllocation: 0,
isPlaceholder: false,
hasStats: false,
+ types: [],
+ inputFieldNames: [],
};
export const ELSER_MODEL_PLACEHOLDER: MlModel = {
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index a9b62b8622cdc..da642ec3e02ff 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -12766,7 +12766,6 @@
"xpack.enterpriseSearch.content.indices.connectorScheduling.page.description": "Votre connecteur est désormais déployé. Vous pouvez planifier du contenu récurrent et accéder aux synchronisations de contrôle ici. Si vous souhaitez exécuter un test rapide, lancez une synchronisation unique à l’aide du bouton {sync}.",
"xpack.enterpriseSearch.content.indices.connectorScheduling.schedulePanel.documentLevelSecurity.dlsDisabledCallout.text": "{link} pour ce connecteur afin d'activer ces options.",
"xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title": "Votre index {indexName} et toute configuration d'ingestion associée ont été supprimés avec succès",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.description": "Aucun de vos modèles entraînés de Machine Learning ne peut être utilisé par un pipeline d'inférence. {documentationLink}",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipeline.missingSourceFieldsDescription": "Champs manquants dans cet index : {commaSeparatedMissingSourceFields}",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText": "Les noms de pipeline sont uniques dans un déploiement, et ils peuvent uniquement contenir des lettres, des chiffres, des traits de soulignement et des traits d'union. Cela créera un pipeline nommé {pipelineName}.",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.fields.descriptionReview": "Vérifiez les mappings des champs du pipeline que vous avez choisi afin de vous assurer que les champs source et cible correspondent à votre cas d'utilisation spécifique. {notEditable}",
@@ -14190,8 +14189,6 @@
"xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFiltersLink": "En savoir plus sur les filtres d'URL",
"xpack.enterpriseSearch.content.indices.extractionRules.editRule.urlLabel": "URL",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.createErrors": "Erreur lors de la création d'un pipeline",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link": "Découvrir comment ajouter un modèle entraîné",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.imageAlt": "Illustration d'absence de modèles de Machine Learning",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.description": "Créez ou réutilisez un pipeline enfant qui servira de processeur dans votre pipeline principal.",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError": "Champ obligatoire.",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipelineLabel": "Sélectionner un pipeline d'inférence existant",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 77335897bd208..a754e68664876 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -12779,7 +12779,6 @@
"xpack.enterpriseSearch.content.indices.connectorScheduling.page.description": "コネクターがデプロイされました。ここで、繰り返しコンテンツとアクセス制御同期をスケジュールします。簡易テストを実行する場合は、{sync}ボタンを使用してワンタイム同期を実行します。",
"xpack.enterpriseSearch.content.indices.connectorScheduling.schedulePanel.documentLevelSecurity.dlsDisabledCallout.text": "これらのオプションを有効にするには、このコネクターの{link}。",
"xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title": "インデックス{indexName}と関連付けられたすべての統合構成が正常に削除されました",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.description": "推論パイプラインで使用できる学習済み機械学習モデルがありません。{documentationLink}",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipeline.missingSourceFieldsDescription": "このインデックスで欠落しているフィールド:{commaSeparatedMissingSourceFields}",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText": "パイプライン名はデプロイ内で一意であり、文字、数字、アンダースコア、ハイフンのみを使用できます。これにより、{pipelineName}という名前のパイプラインが作成されます。",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.fields.descriptionReview": "選択したパイプラインのフィールドマッピングを調べ、ソースフィールドとターゲットフィールドが特定のユースケースに適合していることを確認します。{notEditable}",
@@ -14203,8 +14202,6 @@
"xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFiltersLink": "URLフィルターの詳細をご覧ください",
"xpack.enterpriseSearch.content.indices.extractionRules.editRule.urlLabel": "URL",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.createErrors": "パイプラインの作成エラー",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link": "学習されたモデルの追加方法の詳細",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.imageAlt": "機械学習モデル例がありません",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.description": "メインパイプラインでプロセッサーとして使用される子パイプラインを作成または再利用します。",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError": "フィールドが必要です。",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipelineLabel": "既存の推論パイプラインを選択",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 70fe8f8c91298..ab8c10ed3ed97 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -12873,7 +12873,6 @@
"xpack.enterpriseSearch.content.indices.connectorScheduling.page.description": "现已部署您的连接器。在此处计划重复内容和访问控制同步。如果要运行快速测试,请使用 {sync} 按钮启动一次性同步。",
"xpack.enterpriseSearch.content.indices.connectorScheduling.schedulePanel.documentLevelSecurity.dlsDisabledCallout.text": "此连接器的 {link},用于激活这些选项。",
"xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title": "您的索引 {indexName} 和任何关联的采集配置已成功删除",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.description": "您没有可供推理管道使用的已训练 Machine Learning 模型。{documentationLink}",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipeline.missingSourceFieldsDescription": "此索引中缺少字段:{commaSeparatedMissingSourceFields}",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText": "管道名称在部署内唯一,并且只能包含字母、数字、下划线和连字符。这会创建名为 {pipelineName} 的管道。",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.fields.descriptionReview": "检查您选择的管道的字段映射,确保源和目标字段符合您的特定用例。{notEditable}",
@@ -14297,8 +14296,6 @@
"xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFiltersLink": "详细了解 URL 筛选",
"xpack.enterpriseSearch.content.indices.extractionRules.editRule.urlLabel": "URL",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.createErrors": "创建管道时出错",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link": "了解如何添加已训练模型",
- "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.imageAlt": "无 Machine Learning 模型图示",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.description": "构建或重复使用将在您的主管道中用作处理器的子管道。",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError": "“字段”必填。",
"xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipelineLabel": "选择现有推理管道",
From cb5cf0ebc9b274c40795ea4859af36d9f0dd2db8 Mon Sep 17 00:00:00 2001
From: Cee Chen <549407+cee-chen@users.noreply.github.com>
Date: Thu, 8 Feb 2024 13:58:01 -0800
Subject: [PATCH 051/104] Upgrade EUI to v93.0.0 (#176246)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`v92.2.1` ⏩ `v93.0.0`
---
## [`v93.0.0`](https://github.com/elastic/eui/releases/v93.0.0)
**Bug fixes**
- Fixed `EuiTextTruncate` component to clean up timer from side effect
on unmount ([#7495](https://github.com/elastic/eui/pull/7495))
**Breaking changes**
- Removed deprecated `anchorClassName` prop from `EuiPopover`. Use
`className` instead ([#7488](https://github.com/elastic/eui/pull/7488))
- Removed deprecated `buttonRef` prop from `EuiPopover`. Use
`popoverRef` instead ([#7488](https://github.com/elastic/eui/pull/7488))
- Removed deprecated `toolTipTitle` and `toolTipPosition` props from
`EuiContextMenuItem`. Use `toolTipProps.title` and
`toolTipProps.position` instead
([#7489](https://github.com/elastic/eui/pull/7489))
- Removed deprecated internal `setSelection` ref method from
`EuiInMemoryTable` and `EuiBasicTable`. Use the new controlled
`selection.selected` prop API instead.
([#7491](https://github.com/elastic/eui/pull/7491))
- `EuiTourStep`'s `className` and `style` props now apply to the
anchoring element instead of to the popover panel, to match `EuiPopover`
behavior. ([#7497](https://github.com/elastic/eui/pull/7497))
- Convert your existing usages to `panelClassName` and `panelStyle`
respectively instead.
**Performance**
- Improved the amount of recomputed styles being generated by `EuiCode`
and `EuiCodeBlock` ([#7486](https://github.com/elastic/eui/pull/7486))
**CSS-in-JS conversions**
- Converted `EuiSearchBar` to Emotion
([#7490](https://github.com/elastic/eui/pull/7490))
- Converted `EuiEmptyPrompt` to Emotion
([#7494](https://github.com/elastic/eui/pull/7494))
- Added `euiBorderColor` and `useEuiBorderColorCSS` style utilities
([#7494](https://github.com/elastic/eui/pull/7494))
---------
Co-authored-by: Jon
---
package.json | 2 +-
.../__snapshots__/i18n_service.test.tsx.snap | 6 +-
.../src/i18n_eui_mapping.tsx | 6 +-
src/dev/license_checker/config.ts | 2 +-
.../dashboard_empty_screen.test.tsx.snap | 264 ++++++++----------
.../__snapshots__/data_view.test.tsx.snap | 34 +--
.../discover_tour/discover_tour_provider.tsx | 6 +-
.../visualization_noresults.test.js.snap | 18 +-
.../public/components/search_bar.tsx | 2 +-
.../__snapshots__/policy_table.test.tsx.snap | 70 +++--
.../__snapshots__/no_data.test.js.snap | 220 +++++++--------
.../__snapshots__/page_loading.test.js.snap | 16 +-
.../roles_grid_page.test.tsx.snap | 38 ++-
.../__snapshots__/prompt_page.test.tsx.snap | 4 +-
.../unauthenticated_page.test.tsx.snap | 4 +-
.../reset_session_page.test.tsx.snap | 4 +-
.../landing_page/onboarding/toggle_panel.tsx | 2 +-
.../translations/translations/fr-FR.json | 6 +-
.../translations/translations/ja-JP.json | 6 +-
.../translations/translations/zh-CN.json | 6 +-
yarn.lock | 8 +-
21 files changed, 340 insertions(+), 384 deletions(-)
diff --git a/package.json b/package.json
index 4676515b4f889..05b09aa56196c 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,7 @@
"@elastic/datemath": "5.0.3",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.9.1-canary.1",
"@elastic/ems-client": "8.5.1",
- "@elastic/eui": "92.2.1",
+ "@elastic/eui": "93.0.0",
"@elastic/filesaver": "1.1.2",
"@elastic/node-crypto": "1.2.1",
"@elastic/numeral": "^2.5.1",
diff --git a/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap b/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap
index 78bb5d5d7ea71..50b4b74b0f3e5 100644
--- a/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap
+++ b/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap
@@ -376,9 +376,9 @@ exports[`#start() returns \`Context\` component 1`] = `
"euiToast.dismissToast": "Dismiss toast",
"euiToast.newNotification": "A new notification appears",
"euiToast.notification": "Notification",
- "euiTourStep.closeTour": "Close tour",
- "euiTourStep.endTour": "End tour",
- "euiTourStep.skipTour": "Skip tour",
+ "euiTourFooter.closeTour": "Close tour",
+ "euiTourFooter.endTour": "End tour",
+ "euiTourFooter.skipTour": "Skip tour",
"euiTourStepIndicator.ariaLabel": [Function],
"euiTourStepIndicator.isActive": "active",
"euiTourStepIndicator.isComplete": "complete",
diff --git a/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx b/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx
index 8710a8640d747..a8c4db74ba406 100644
--- a/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx
+++ b/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx
@@ -1710,13 +1710,13 @@ export const getEuiContextMapping = (): EuiTokensObject => {
defaultMessage: 'Notification',
description: 'ARIA label on an element containing a notification',
}),
- 'euiTourStep.endTour': i18n.translate('core.euiTourStep.endTour', {
+ 'euiTourFooter.endTour': i18n.translate('core.euiTourFooter.endTour', {
defaultMessage: 'End tour',
}),
- 'euiTourStep.skipTour': i18n.translate('core.euiTourStep.skipTour', {
+ 'euiTourFooter.skipTour': i18n.translate('core.euiTourFooter.skipTour', {
defaultMessage: 'Skip tour',
}),
- 'euiTourStep.closeTour': i18n.translate('core.euiTourStep.closeTour', {
+ 'euiTourFooter.closeTour': i18n.translate('core.euiTourFooter.closeTour', {
defaultMessage: 'Close tour',
}),
'euiTourStepIndicator.isActive': i18n.translate('core.euiTourStepIndicator.isActive', {
diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts
index 436fc25abf955..6b1be5c45934a 100644
--- a/src/dev/license_checker/config.ts
+++ b/src/dev/license_checker/config.ts
@@ -85,7 +85,7 @@ export const LICENSE_OVERRIDES = {
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
'@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint
'@elastic/ems-client@8.5.1': ['Elastic License 2.0'],
- '@elastic/eui@92.2.1': ['SSPL-1.0 OR Elastic License 2.0'],
+ '@elastic/eui@93.0.0': ['SSPL-1.0 OR Elastic License 2.0'],
'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry
'buffers@0.1.1': ['MIT'], // license in importing module https://www.npmjs.com/package/binary
'@bufbuild/protobuf@1.2.1': ['Apache-2.0'], // license (Apache-2.0 AND BSD-3-Clause)
diff --git a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
index 6863540ad4a71..d478b18ef2ddb 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
+++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
@@ -20,13 +20,13 @@ exports[`DashboardEmptyScreen renders correctly with edit mode 1`] = `
class="emotion-euiPageSection__content-l-center"
>