From 2ce942488b8643ba55086cdd7dd63ebd12c92800 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Wed, 28 Oct 2020 10:44:25 -0400 Subject: [PATCH 01/73] [Resolver] Enable resolver test plugin tests (#81339) Resolver has a test plugin. It can be found in `x-pack/tests/plugin_functional`. You can try it out like this: ``` yarn start --plugin-path x-pack/test/plugin_functional/plugins/resolver_test/ ``` This PR enables automated tests for the test plugin. This ensures that the test plugin will render. --- x-pack/scripts/functional_tests.js | 1 + x-pack/test/plugin_functional/config.ts | 2 +- .../test_suites/global_search/global_search_bar.ts | 3 ++- .../test_suites/global_search/index.ts | 3 ++- .../plugin_functional/test_suites/resolver/index.ts | 12 ++++++------ 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index b15a2cf8d1f1d..ad6ec7bf82282 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -7,6 +7,7 @@ const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), require.resolve('../test/security_solution_endpoint/config.ts'), + require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/functional_with_es_ssl/config.ts'), require.resolve('../test/functional/config_security_basic.ts'), require.resolve('../test/security_functional/login_selector.config.ts'), diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index e7d96023f3653..37d35662eb15b 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -59,7 +59,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { apps: { ...xpackFunctionalConfig.get('apps'), resolverTest: { - pathname: '/app/resolver_test', + pathname: '/app/resolverTest', }, }, diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts index 2b7ae3e576590..005d516e2943c 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - describe('GlobalSearchBar', function () { + // See: https://github.com/elastic/kibana/issues/81397 + describe.skip('GlobalSearchBar', function () { const { common } = getPageObjects(['common']); const find = getService('find'); const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/plugin_functional/test_suites/global_search/index.ts b/x-pack/test/plugin_functional/test_suites/global_search/index.ts index a54e6933be69b..f43e293c30fd6 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/index.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/index.ts @@ -7,7 +7,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('GlobalSearch API', function () { + // See https://github.com/elastic/kibana/issues/81397 + describe.skip('GlobalSearch API', function () { this.tags('ciGroup7'); loadTestFile(require.resolve('./global_search_api')); loadTestFile(require.resolve('./global_search_providers')); diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts index 9cc2751a4287d..8cdf54a50bc53 100644 --- a/x-pack/test/plugin_functional/test_suites/resolver/index.ts +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -10,18 +10,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); - describe('Resolver embeddable test app', function () { + describe('Resolver test app', function () { this.tags('ciGroup7'); beforeEach(async function () { await pageObjects.common.navigateToApp('resolverTest'); }); - it('renders a container div for the embeddable', async function () { - await testSubjects.existOrFail('resolverEmbeddableContainer'); - }); - it('renders resolver', async function () { - await testSubjects.existOrFail('resolverEmbeddable'); + it('renders at least one node, one node-list, one edge line, and graph controls', async function () { + await testSubjects.existOrFail('resolver:node'); + await testSubjects.existOrFail('resolver:node-list'); + await testSubjects.existOrFail('resolver:graph:edgeline'); + await testSubjects.existOrFail('resolver:graph-controls'); }); }); } From 333f8732de2ea962dda8020d82aa4f57eb0a3591 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 28 Oct 2020 10:38:23 -0500 Subject: [PATCH 02/73] APM Experiments settings (#81554) * Add advanced settings for APM * Register advanced settings in server startup that show in the Kibana advanced settings UI. (Fixes #81396.) * Format settings pages to be more consistent. --- x-pack/plugins/apm/common/ui_settings_keys.ts | 8 +++ .../app/ServiceDetails/ServiceDetailTabs.tsx | 49 ++++++++++++------- .../Settings/AgentConfigurations/index.tsx | 11 ++++- .../app/Settings/ApmIndices/index.test.tsx | 6 +-- .../app/Settings/ApmIndices/index.tsx | 46 ++++++++--------- .../CustomLink/CreateCustomLinkButton.tsx | 2 +- .../Settings/CustomizeUI/CustomLink/Title.tsx | 23 ++------- .../Settings/CustomizeUI/CustomLink/index.tsx | 24 ++++++--- .../Settings/anomaly_detection/jobs_list.tsx | 2 +- x-pack/plugins/apm/readme.md | 41 +++++++++++----- x-pack/plugins/apm/server/plugin.ts | 4 ++ x-pack/plugins/apm/server/ui_settings.ts | 48 ++++++++++++++++++ 12 files changed, 180 insertions(+), 84 deletions(-) create mode 100644 x-pack/plugins/apm/common/ui_settings_keys.ts create mode 100644 x-pack/plugins/apm/server/ui_settings.ts diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts new file mode 100644 index 0000000000000..38922fa445a47 --- /dev/null +++ b/x-pack/plugins/apm/common/ui_settings_keys.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const enableCorrelations = 'apm:enableCorrelations'; +export const enableServiceOverview = 'apm:enableServiceOverview'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index cbb6d9a8fbe41..76c8a289b830c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -8,6 +8,7 @@ import { EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; +import { enableServiceOverview } from '../../../../common/ui_settings_keys'; import { useAgentName } from '../../../hooks/useAgentName'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { EuiTabLink } from '../../shared/EuiTabLink'; @@ -29,7 +30,19 @@ interface Props { export function ServiceDetailTabs({ serviceName, tab }: Props) { const { agentName } = useAgentName(); - const { serviceMapEnabled } = useApmPluginContext().config; + const { uiSettings } = useApmPluginContext().core; + + const overviewTab = { + link: ( + + {i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', { + defaultMessage: 'Overview', + })} + + ), + render: () => <>, + name: 'overview', + }; const transactionsTab = { link: ( @@ -57,7 +70,23 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { name: 'errors', }; - const tabs = [transactionsTab, errorsTab]; + const serviceMapTab = { + link: ( + + {i18n.translate('xpack.apm.home.serviceMapTabLabel', { + defaultMessage: 'Service Map', + })} + + ), + render: () => , + name: 'service-map', + }; + + const tabs = [transactionsTab, errorsTab, serviceMapTab]; + + if (uiSettings.get(enableServiceOverview)) { + tabs.unshift(overviewTab); + } if (isJavaAgentName(agentName)) { const nodesListTab = { @@ -89,22 +118,6 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { tabs.push(metricsTab); } - const serviceMapTab = { - link: ( - - {i18n.translate('xpack.apm.home.serviceMapTabLabel', { - defaultMessage: 'Service Map', - })} - - ), - render: () => , - name: 'service-map', - }; - - if (serviceMapEnabled) { - tabs.push(serviceMapTab); - } - const selectedTab = tabs.find((serviceTab) => serviceTab.name === tab); return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 8e32c55da9161..dfc78028c3596 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { EuiButton, EuiFlexGroup, @@ -37,6 +36,14 @@ export function AgentConfigurations() { return ( <> + +

+ {i18n.translate('xpack.apm.agentConfig.titleText', { + defaultMessage: 'Agent remote configuration', + })} +

+
+ @@ -44,7 +51,7 @@ export function AgentConfigurations() {

{i18n.translate( 'xpack.apm.agentConfig.configurationsPanelTitle', - { defaultMessage: 'Agent remote configuration' } + { defaultMessage: 'Configurations' } )}

diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx index 68e75d595363d..53794ca9965ff 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx @@ -24,11 +24,11 @@ describe('ApmIndices', () => { ); expect(getByText('Indices')).toMatchInlineSnapshot(` -

Indices -

+ `); expect(spy).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 145fb9683cb61..fac947b3ec68e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -163,23 +163,24 @@ export function ApmIndices() { }; return ( - - - - -

- {i18n.translate('xpack.apm.settings.apmIndices.title', { - defaultMessage: 'Indices', - })} -

-
- - -

- {i18n.translate('xpack.apm.settings.apmIndices.description', { - defaultMessage: `The APM UI uses index patterns to query your APM indices. If you've customized the index names that APM Server writes events to, you may need to update these patterns for the APM UI to work. Settings here take precedence over those set in kibana.yml.`, - })} -

+ <> + +

+ {i18n.translate('xpack.apm.settings.apmIndices.title', { + defaultMessage: 'Indices', + })} +

+
+ + + {i18n.translate('xpack.apm.settings.apmIndices.description', { + defaultMessage: `The APM UI uses index patterns to query your APM indices. If you've customized the index names that APM Server writes events to, you may need to update these patterns for the APM UI to work. Settings here take precedence over those set in kibana.yml.`, + })} + + + + + {APM_INDEX_LABELS.map(({ configurationName, label }) => { const matchedConfiguration = data.find( @@ -239,11 +240,10 @@ export function ApmIndices() { -
-
-
- - -
+
+
+ +
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx index 2e860ebe22c0f..56b3eaf425af7 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export function CreateCustomLinkButton({ onClick }: { onClick: () => void }) { return ( - + {i18n.translate( 'xpack.apm.settings.customizeUI.customLink.createCustomLink', { defaultMessage: 'Create custom link' } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx index 22d8749d78834..2017aa42e1c5a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -11,28 +12,14 @@ export function Title() { return ( - + -

+

{i18n.translate('xpack.apm.settings.customizeUI.customLink', { defaultMessage: 'Custom Links', })} -

-
- - - +
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 45a7fa2a118f2..a7d7cf40ba849 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -4,19 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; +import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; import { useLicense } from '../../../../../hooks/useLicense'; -import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { LicensePrompt } from '../../../../shared/LicensePrompt'; +import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { CustomLinkFlyout } from './CustomLinkFlyout'; import { CustomLinkTable } from './CustomLinkTable'; import { EmptyPrompt } from './EmptyPrompt'; import { Title } from './Title'; -import { CreateCustomLinkButton } from './CreateCustomLinkButton'; -import { LicensePrompt } from '../../../../shared/LicensePrompt'; export function CustomLinkOverview() { const license = useLicense(); @@ -82,8 +89,13 @@ export function CustomLinkOverview() {
)}
- - + + + {i18n.translate('xpack.apm.settings.customizeUI.customLink.info', { + defaultMessage: + 'These links will be shown in the Actions context menu for transactions.', + })} + {hasValidLicense ? ( showEmptyPrompt ? ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 6e95df0dddd84..137dcfcdbb4f0 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -80,7 +80,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - + {i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.addEnvironments', { diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index d6fdb5f52291c..0adfb99e7164e 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -1,8 +1,8 @@ # Documentation for APM UI developers -### Setup local environment +## Local environment setup -#### Kibana +### Kibana ``` git clone git@github.com:elastic/kibana.git @@ -11,15 +11,15 @@ yarn kbn bootstrap yarn start --no-base-path ``` -#### APM Server, Elasticsearch and data +### APM Server, Elasticsearch and data To access an elasticsearch instance that has live data you have two options: -##### A. Connect to Elasticsearch on Cloud (internal devs only) +#### A. Connect to Elasticsearch on Cloud (internal devs only) Find the credentials for the cluster [here](https://github.com/elastic/apm-dev/blob/master/docs/credentials/apm-ui-clusters.md#apmelstcco) -##### B. Start Elastic Stack and APM data generators +#### B. Start Elastic Stack and APM data generators ``` git clone git@github.com:elastic/apm-integration-testing.git @@ -29,6 +29,8 @@ cd apm-integration-testing/ _Docker Compose is required_ +## Testing + ### E2E (Cypress) tests ```sh @@ -109,23 +111,23 @@ The API tests for "trial" are located in `x-pack/test/apm_api_integration/trial/ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) -### Linting +## Linting _Note: Run the following commands from `kibana/`._ -#### Prettier +### Prettier ``` yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write ``` -#### ESLint +### ESLint ``` yarn eslint ./x-pack/plugins/apm --fix ``` -### Setup default APM users +## Setup default APM users APM behaves differently depending on which the role and permissions a logged in user has. For testing purposes APM uses 3 custom users: @@ -144,20 +146,35 @@ node x-pack/plugins/apm/scripts/setup-kibana-security.js --role-suffix Advanced Settings > Observability. + +## Further resources - [Cypress integration tests](./e2e/README.md) - [VSCode setup instructions](./dev_docs/vscode_setup.md) diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b417f8689b229..d3341b6c1b163 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { i18n } from '@kbn/i18n'; import { combineLatest, Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -36,6 +37,7 @@ import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_cust import { createApmApi } from './routes/create_apm_api'; import { apmIndices, apmTelemetry } from './saved_objects'; import { createElasticCloudInstructions } from './tutorial/elastic_cloud'; +import { uiSettings } from './ui_settings'; export interface APMPluginSetup { config$: Observable; @@ -75,6 +77,8 @@ export class APMPlugin implements Plugin { core.savedObjects.registerType(apmIndices); core.savedObjects.registerType(apmTelemetry); + core.uiSettings.register(uiSettings); + if (plugins.actions && plugins.alerts) { registerApmAlerts({ alerts: plugins.alerts, diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts new file mode 100644 index 0000000000000..fe5b11d89d716 --- /dev/null +++ b/x-pack/plugins/apm/server/ui_settings.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { + enableCorrelations, + enableServiceOverview, +} from '../common/ui_settings_keys'; + +/** + * uiSettings definitions for APM. + */ +export const uiSettings: Record> = { + [enableCorrelations]: { + category: ['Observability'], + name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', { + defaultMessage: 'APM Correlations', + }), + value: false, + description: i18n.translate( + 'xpack.apm.enableCorrelationsExperimentDescription', + { + defaultMessage: + 'Enable the experimental correlations UI and API endpoint in APM.', + } + ), + schema: schema.boolean(), + }, + [enableServiceOverview]: { + category: ['Observability'], + name: i18n.translate('xpack.apm.enableServiceOverviewExperimentName', { + defaultMessage: 'APM Service overview', + }), + value: false, + description: i18n.translate( + 'xpack.apm.enableServiceOverviewExperimentDescription', + { + defaultMessage: 'Enable the Overview tab for services in APM.', + } + ), + schema: schema.boolean(), + }, +}; From ac70e1e944b82a7d73272fa3e1b496bb32234122 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 28 Oct 2020 17:10:49 +0100 Subject: [PATCH 03/73] [ILM] Migrate Cold phase to Form Lib (#81754) * use form lib fields and start updating deserializer * delete legacy data tier allocation field * finished deserialization * delete legacy serialization, validation and deserialization * fix type issue and remove propertyOf for now * fix legacy tests and create another number validator * added serialization test coverage * fix https://github.com/elastic/kibana/issues/81697 * clean up remaining legacy tests and slight update to existing test * remove legacy unused components * fix copy to be clearer for more scenarios * remove remaining coldphase interface use and clean up unused i18n * update default index priority for cold phase * updated cold phase index priority to 0 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/constants.ts | 25 ++ .../edit_policy/edit_policy.helpers.tsx | 22 +- .../edit_policy/edit_policy.test.ts | 184 +++++++++++--- .../client_integration/helpers/index.ts | 1 + .../__jest__/components/edit_policy.test.tsx | 47 ++-- .../common/types/policies.ts | 9 - .../public/application/constants/policy.ts | 13 +- .../cloud_data_tier_callout.tsx | 54 ---- .../data_tier_allocation.scss | 9 - .../data_tier_allocation.tsx | 184 -------------- .../default_allocation_notice.tsx | 106 -------- .../components/data_tier_allocation/index.ts | 13 - .../no_node_attributes_warning.tsx | 50 ---- .../data_tier_allocation/node_allocation.tsx | 121 --------- .../node_attrs_details.tsx | 106 -------- .../node_data_provider.tsx | 70 ------ .../components/data_tier_allocation/types.ts | 28 --- .../components/forcemerge_legacy.tsx | 131 ---------- .../sections/edit_policy/components/index.ts | 9 - .../components/phases/cold_phase.tsx | 233 ------------------ .../phases/cold_phase/cold_phase.tsx | 187 ++++++++++++++ .../components/phases/cold_phase}/index.ts | 2 +- .../components/no_node_attributes_warning.tsx | 4 +- .../data_tier_allocation_legacy_field.tsx | 141 ----------- .../components/phases/shared/index.ts | 2 - .../phases/warm_phase/warm_phase.tsx | 15 +- .../components/policy_json_flyout.tsx | 1 + .../components/set_priority_input_legacy.tsx | 85 ------- .../sections/edit_policy/deserializer.ts | 39 ++- .../sections/edit_policy/edit_policy.tsx | 14 +- .../sections/edit_policy/form_schema.ts | 75 +++++- .../sections/edit_policy/form_validations.ts | 38 ++- .../sections/edit_policy/serializer.ts | 36 +++ .../application/sections/edit_policy/types.ts | 22 +- .../services/policies/cold_phase.ts | 154 ------------ .../policies/policy_serialization.test.ts | 123 +-------- .../services/policies/policy_serialization.ts | 13 +- .../services/policies/policy_validation.ts | 14 +- .../shared/serialize_phase_with_allocation.ts | 42 ---- .../application/services/ui_metric.test.ts | 5 +- .../public/application/services/ui_metric.ts | 4 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 43 files changed, 614 insertions(+), 1823 deletions(-) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge_legacy.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx rename x-pack/plugins/index_lifecycle_management/public/application/{services/policies/shared => sections/edit_policy/components/phases/cold_phase}/index.ts (74%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input_legacy.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 0a96146339a58..3d430cf31621e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -30,6 +30,31 @@ export const DEFAULT_POLICY: PolicyFromES = { name: 'my_policy', }; +export const POLICY_WITH_MIGRATE_OFF: PolicyFromES = { + version: 1, + modified_date: Date.now().toString(), + policy: { + name: 'my_policy', + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + warm: { + actions: { + migrate: { enabled: false }, + }, + }, + }, + }, + name: 'my_policy', +}; + export const POLICY_WITH_INCLUDE_EXCLUDE: PolicyFromES = { version: 1, modified_date: Date.now().toString(), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 1716f124b0c83..ad61641ea1e36 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -154,12 +154,12 @@ export const setup = async () => { component.update(); }; - const setSelectedNodeAttribute = (phase: string) => + const setSelectedNodeAttribute = (phase: Phases) => createFormSetValueAction(`${phase}-selectedNodeAttrs`); - const setReplicas = async (value: string) => { - await createFormToggleAction('warm-setReplicasSwitch')(true); - await createFormSetValueAction('warm-selectedReplicaCount')(value); + const setReplicas = (phase: Phases) => async (value: string) => { + await createFormToggleAction(`${phase}-setReplicasSwitch`)(true); + await createFormSetValueAction(`${phase}-selectedReplicaCount`)(value); }; const setShrink = async (value: string) => { @@ -167,6 +167,8 @@ export const setup = async () => { await createFormSetValueAction('warm-selectedPrimaryShardCount')(value); }; + const setFreeze = createFormToggleAction('freezeSwitch'); + return { ...testBed, actions: { @@ -189,13 +191,23 @@ export const setup = async () => { setMinAgeUnits: setMinAgeUnits('warm'), setDataAllocation: setDataAllocation('warm'), setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), - setReplicas, + setReplicas: setReplicas('warm'), setShrink, toggleForceMerge: toggleForceMerge('warm'), setForcemergeSegments: setForcemergeSegmentsCount('warm'), setBestCompression: setBestCompression('warm'), setIndexPriority: setIndexPriority('warm'), }, + cold: { + enable: enable('cold'), + setMinAgeValue: setMinAgeValue('cold'), + setMinAgeUnits: setMinAgeUnits('cold'), + setDataAllocation: setDataAllocation('cold'), + setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), + setReplicas: setReplicas('cold'), + setFreeze, + setIndexPriority: setIndexPriority('cold'), + }, }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index fccffde3f793f..11fadf51f27f8 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -15,6 +15,7 @@ import { NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME, DEFAULT_POLICY, + POLICY_WITH_MIGRATE_OFF, POLICY_WITH_INCLUDE_EXCLUDE, POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, POLICY_WITH_NODE_ROLE_ALLOCATION, @@ -199,26 +200,6 @@ describe('', () => { `); }); - test('default allocation with replicas set', async () => { - const { actions } = testBed; - await actions.warm.enable(true); - await actions.warm.setReplicas('123'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm - .actions; - expect(warmPhaseActions).toMatchInlineSnapshot(` - Object { - "allocate": Object { - "number_of_replicas": 123, - }, - "set_priority": Object { - "priority": 50, - }, - } - `); - }); - test('setting warm phase on rollover to "true"', async () => { const { actions } = testBed; await actions.warm.enable(true); @@ -252,6 +233,7 @@ describe('', () => { test('preserves include, exclude allocation settings', async () => { const { actions } = testBed; await actions.warm.setDataAllocation('node_attrs'); + await actions.warm.setSelectedNodeAttribute('test:123'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; const warmPhaseAllocate = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm @@ -264,6 +246,101 @@ describe('', () => { "include": Object { "abc": "123", }, + "require": Object { + "test": "123", + }, + } + `); + }); + }); + }); + + describe('cold phase', () => { + describe('serialization', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('default values', async () => { + const { actions } = testBed; + + await actions.cold.enable(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.cold).toMatchInlineSnapshot(` + Object { + "actions": Object { + "set_priority": Object { + "priority": 0, + }, + }, + "min_age": "0d", + } + `); + }); + + test('setting all values', async () => { + const { actions } = testBed; + + await actions.cold.enable(true); + await actions.cold.setMinAgeValue('123'); + await actions.cold.setMinAgeUnits('s'); + await actions.cold.setDataAllocation('node_attrs'); + await actions.cold.setSelectedNodeAttribute('test:123'); + await actions.cold.setReplicas('123'); + await actions.cold.setFreeze(true); + await actions.cold.setIndexPriority('123'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "cold": Object { + "actions": Object { + "allocate": Object { + "number_of_replicas": 123, + "require": Object { + "test": "123", + }, + }, + "freeze": Object {}, + "set_priority": Object { + "priority": 123, + }, + }, + "min_age": "123s", + }, + "hot": Object { + "actions": Object { + "rollover": Object { + "max_age": "30d", + "max_size": "50gb", + }, + "set_priority": Object { + "priority": 100, + }, + }, + "min_age": "0ms", + }, + }, } `); }); @@ -385,6 +462,33 @@ describe('', () => { }); describe('data allocation', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('setting node_attr based allocation, but not selecting node attribute', async () => { + const { actions } = testBed; + await actions.warm.setDataAllocation('node_attrs'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; + + expect(warmPhase.actions.migrate).toEqual({ enabled: false }); + }); + describe('node roles', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]); @@ -401,15 +505,32 @@ describe('', () => { const { component } = testBed; component.update(); }); - test('showing "default" type', () => { + + test('detecting use of the recommended allocation type', () => { const { find } = testBed; - expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toContain( - 'recommended' - ); - expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).not.toContain( - 'Custom' - ); - expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).not.toContain('Off'); + const selectedDataAllocation = find( + 'warm-dataTierAllocationControls.dataTierSelect' + ).text(); + expect(selectedDataAllocation).toBe('Use warm nodes (recommended)'); + }); + + test('setting replicas serialization', async () => { + const { actions } = testBed; + await actions.warm.setReplicas('123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm + .actions; + expect(warmPhaseActions).toMatchInlineSnapshot(` + Object { + "allocate": Object { + "number_of_replicas": 123, + }, + "set_priority": Object { + "priority": 50, + }, + } + `); }); }); describe('node attr and none', () => { @@ -429,9 +550,12 @@ describe('', () => { component.update(); }); - test('showing "custom" and "off" types', () => { + test('detecting use of the custom allocation type', () => { + const { find } = testBed; + expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); + }); + test('detecting use of the "off" allocation type', () => { const { find } = testBed; - expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toContain('Custom'); expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index c6d27ca890b54..e8ebc2963d16a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -17,4 +17,5 @@ export type TestSubjects = | 'hot-selectedMaxDocuments' | 'hot-selectedMaxAge' | 'hot-selectedMaxAgeUnits' + | 'freezeSwitch' | string; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 4ba6cee7b027f..4a3fedfb264ac 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -119,9 +119,6 @@ const noRollover = async (rendered: ReactWrapper) => { }); rendered.update(); }; -const getNodeAttributeSelectLegacy = (rendered: ReactWrapper, phase: string) => { - return rendered.find(`select#${phase}-selectedNodeAttrs`); -}; const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { return findTestSubject(rendered, `${phase}-selectedNodeAttrs`); }; @@ -142,15 +139,6 @@ const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: strin }); rendered.update(); }; -const setPhaseIndexPriorityLegacy = ( - rendered: ReactWrapper, - phase: string, - priority: string | number -) => { - const priorityInput = rendered.find(`input#${phase}-phaseIndexPriority`); - priorityInput.simulate('change', { target: { value: priority } }); - rendered.update(); -}; const setPhaseIndexPriority = async ( rendered: ReactWrapper, phase: string, @@ -184,7 +172,7 @@ describe('edit policy', () => { */ const waitForFormLibValidation = (rendered: ReactWrapper) => { act(() => { - jest.advanceTimersByTime(1000); + jest.runAllTimers(); }); rendered.update(); }; @@ -394,7 +382,7 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await setPhaseIndexPriority(rendered, 'hot', '-1'); waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); }); describe('warm phase', () => { @@ -519,7 +507,7 @@ describe('edit policy', () => { await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelectLegacy(rendered, 'warm').exists()).toBeFalsy(); + expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); test('should show warning instead of node attributes input when none exist', async () => { http.setupNodeListResponse({ @@ -534,7 +522,7 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); await openNodeAttributesSection(rendered, 'warm'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelectLegacy(rendered, 'warm').exists()).toBeFalsy(); + expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); test('should show node attributes input when attributes exist', async () => { const rendered = mountWithIntl(component); @@ -625,8 +613,9 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfterLegacy(rendered, 'cold', '0'); - await save(rendered); + await setPhaseAfter(rendered, 'cold', '0'); + waitForFormLibValidation(rendered); + rendered.update(); expectedErrorMessages(rendered, []); }); test('should show positive number required error when trying to save cold phase with -1 for after', async () => { @@ -634,9 +623,9 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfterLegacy(rendered, 'cold', '-1'); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + await setPhaseAfter(rendered, 'cold', '-1'); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); test('should show spinner for node attributes input when loading', async () => { server.respondImmediately = false; @@ -646,7 +635,7 @@ describe('edit policy', () => { await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelectLegacy(rendered, 'cold').exists()).toBeFalsy(); + expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); test('should show warning instead of node attributes input when none exist', async () => { http.setupNodeListResponse({ @@ -661,7 +650,7 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); await openNodeAttributesSection(rendered, 'cold'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelectLegacy(rendered, 'cold').exists()).toBeFalsy(); + expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); test('should show node attributes input when attributes exist', async () => { const rendered = mountWithIntl(component); @@ -671,7 +660,7 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); await openNodeAttributesSection(rendered, 'cold'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelectLegacy(rendered, 'cold'); + const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(nodeAttributesSelect.find('option').length).toBe(2); }); @@ -683,7 +672,7 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); await openNodeAttributesSection(rendered, 'cold'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelectLegacy(rendered, 'cold'); + const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); expect(nodeAttributesSelect.find('option').length).toBe(2); @@ -702,10 +691,10 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfterLegacy(rendered, 'cold', '1'); - setPhaseIndexPriorityLegacy(rendered, 'cold', '-1'); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + await setPhaseAfter(rendered, 'cold', '1'); + await setPhaseIndexPriority(rendered, 'cold', '-1'); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); test('should show default allocation warning when no node roles are found', async () => { http.setupNodeListResponse({ diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 813fcd9c253f1..5692decbbf7a8 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -116,7 +116,6 @@ export interface ForcemergeAction { export interface LegacyPolicy { name: string; phases: { - cold: ColdPhase; delete: DeletePhase; }; } @@ -159,14 +158,6 @@ export interface PhaseWithForcemergeAction { bestCompressionEnabled: boolean; } -export interface ColdPhase - extends CommonPhaseSettings, - PhaseWithMinAge, - PhaseWithAllocationAction, - PhaseWithIndexPriority { - freezeEnabled: boolean; -} - export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge { waitForSnapshotPolicy: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 136b68727672a..23d7387aa7076 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SerializedPhase, ColdPhase, DeletePhase, SerializedPolicy } from '../../../common/types'; +import { SerializedPhase, DeletePhase, SerializedPolicy } from '../../../common/types'; export const defaultSetPriority: string = '100'; @@ -24,17 +24,6 @@ export const defaultPolicy: SerializedPolicy = { }, }; -export const defaultNewColdPhase: ColdPhase = { - phaseEnabled: false, - selectedMinimumAge: '0', - selectedMinimumAgeUnits: 'd', - selectedNodeAttrs: '', - selectedReplicaCount: '', - freezeEnabled: false, - phaseIndexPriority: '0', - dataTierAllocationType: 'default', -}; - export const defaultNewDeletePhase: DeletePhase = { phaseEnabled: false, selectedMinimumAge: '0', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx deleted file mode 100644 index fc87b553ba521..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx +++ /dev/null @@ -1,54 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent } from 'react'; -import { EuiCallOut, EuiLink } from '@elastic/eui'; - -import { useKibana } from '../../../../../shared_imports'; - -const deployment = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body.elasticDeploymentLink', - { - defaultMessage: 'deployment', - } -); - -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.coldTierTitle', { - defaultMessage: 'Create a cold tier', - }), - body: (deploymentUrl?: string) => { - return ( - - {deployment} - - ) : ( - deployment - ), - }} - /> - ); - }, -}; - -export const CloudDataTierCallout: FunctionComponent = () => { - const { - services: { cloud }, - } = useKibana(); - - return ( - - {i18nTexts.body(cloud?.cloudDeploymentUrl)} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss deleted file mode 100644 index 62ec3f303e1e8..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss +++ /dev/null @@ -1,9 +0,0 @@ -.indexLifecycleManagement__phase__dataTierAllocation { - &__controlSection { - background-color: $euiColorLightestShade; - padding-top: $euiSizeM; - padding-left: $euiSizeM; - padding-right: $euiSizeM; - padding-bottom: $euiSizeM; - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx deleted file mode 100644 index f58f36fc45a0c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx +++ /dev/null @@ -1,184 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiText, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; - -import { DataTierAllocationType } from '../../../../../../common/types'; -import { NodeAllocation } from './node_allocation'; -import { SharedProps } from './types'; - -import './data_tier_allocation.scss'; - -type SelectOptions = EuiSuperSelectOption; - -const i18nTexts = { - allocationFieldLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel', - { defaultMessage: 'Data tier options' } - ), - allocationOptions: { - warm: { - default: { - input: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input', - { defaultMessage: 'Use warm nodes (recommended)' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText', - { defaultMessage: 'Move data to nodes in the warm tier.' } - ), - }, - none: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input', - { defaultMessage: 'Off' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText', - { defaultMessage: 'Do not move data in the warm phase.' } - ), - }, - custom: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input', - { defaultMessage: 'Custom' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } - ), - }, - }, - cold: { - default: { - input: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input', - { defaultMessage: 'Use cold nodes (recommended)' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText', - { defaultMessage: 'Move data to nodes in the cold tier.' } - ), - }, - none: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input', - { defaultMessage: 'Off' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText', - { defaultMessage: 'Do not move data in the cold phase.' } - ), - }, - custom: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input', - { defaultMessage: 'Custom' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } - ), - }, - }, - }, -}; - -export const DataTierAllocation: FunctionComponent = (props) => { - const { phaseData, setPhaseData, phase, hasNodeAttributes, disableDataTierOption } = props; - - useEffect(() => { - if (disableDataTierOption && phaseData.dataTierAllocationType === 'default') { - /** - * @TODO - * This is a slight hack because we only know we should disable the "default" option further - * down the component tree (i.e., after the policy has been deserialized). - * - * We reset the value to "custom" if we deserialized to "default". - * - * It would be better if we had all the information we needed before deserializing and - * were able to handle this at the deserialization step instead of patching further down - * the component tree - this should be a future refactor. - */ - setPhaseData('dataTierAllocationType', 'custom'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
- - setPhaseData('dataTierAllocationType', value)} - options={ - [ - disableDataTierOption - ? undefined - : { - 'data-test-subj': 'defaultDataAllocationOption', - value: 'default', - inputDisplay: i18nTexts.allocationOptions[phase].default.input, - dropdownDisplay: ( - <> - {i18nTexts.allocationOptions[phase].default.input} - -

- {i18nTexts.allocationOptions[phase].default.helpText} -

-
- - ), - }, - { - 'data-test-subj': 'customDataAllocationOption', - value: 'custom', - inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay, - dropdownDisplay: ( - <> - {i18nTexts.allocationOptions[phase].custom.inputDisplay} - -

- {i18nTexts.allocationOptions[phase].custom.helpText} -

-
- - ), - }, - { - 'data-test-subj': 'noneDataAllocationOption', - value: 'none', - inputDisplay: i18nTexts.allocationOptions[phase].none.inputDisplay, - dropdownDisplay: ( - <> - {i18nTexts.allocationOptions[phase].none.inputDisplay} - -

- {i18nTexts.allocationOptions[phase].none.helpText} -

-
- - ), - }, - ].filter(Boolean) as SelectOptions[] - } - /> -
- {phaseData.dataTierAllocationType === 'custom' && hasNodeAttributes && ( - <> - -
- -
- - )} -
- ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx deleted file mode 100644 index 3d0052c69607b..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx +++ /dev/null @@ -1,106 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; - -import { PhaseWithAllocation, DataTierRole } from '../../../../../../common/types'; - -import { AllocationNodeRole } from '../../../../lib'; - -const i18nTextsNodeRoleToDataTier: Record = { - data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { - defaultMessage: 'hot', - }), - data_warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel', { - defaultMessage: 'warm', - }), - data_cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel', { - defaultMessage: 'cold', - }), -}; - -const i18nTexts = { - notice: { - warm: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title', - { defaultMessage: 'No nodes assigned to the warm tier' } - ), - body: (nodeRole: DataTierRole) => - i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm', { - defaultMessage: - 'This policy will move data in the warm phase to {tier} tier nodes instead.', - values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, - }), - }, - cold: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold.title', - { defaultMessage: 'No nodes assigned to the cold tier' } - ), - body: (nodeRole: DataTierRole) => - i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold', { - defaultMessage: - 'This policy will move data in the cold phase to {tier} tier nodes instead.', - values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, - }), - }, - }, - warning: { - warm: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', - { defaultMessage: 'No nodes assigned to the warm tier' } - ), - body: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', - { - defaultMessage: - 'Assign at least one node to the warm or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', - } - ), - }, - cold: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', - { defaultMessage: 'No nodes assigned to the cold tier' } - ), - body: i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', - { - defaultMessage: - 'Assign at least one node to the cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', - } - ), - }, - }, -}; - -interface Props { - phase: PhaseWithAllocation; - targetNodeRole: AllocationNodeRole; -} - -export const DefaultAllocationNotice: FunctionComponent = ({ phase, targetNodeRole }) => { - const content = - targetNodeRole === 'none' ? ( - - {i18nTexts.warning[phase].body} - - ) : ( - - {i18nTexts.notice[phase].body(targetNodeRole)} - - ); - - return content; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts deleted file mode 100644 index 937e3dd28da97..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts +++ /dev/null @@ -1,13 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { NodesDataProvider } from './node_data_provider'; -export { NodeAllocation } from './node_allocation'; -export { NodeAttrsDetails } from './node_attrs_details'; -export { DataTierAllocation } from './data_tier_allocation'; -export { DefaultAllocationNotice } from './default_allocation_notice'; -export { NoNodeAttributesWarning } from './no_node_attributes_warning'; -export { CloudDataTierCallout } from './cloud_data_tier_callout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx deleted file mode 100644 index 69185277f64ce..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx +++ /dev/null @@ -1,50 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { PhaseWithAllocation } from '../../../../../../common/types'; - -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel', { - defaultMessage: 'No custom node attributes configured', - }), - warm: { - body: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription', - { - defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Warm nodes will be used instead.', - } - ), - }, - cold: { - body: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription', - { - defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Cold nodes will be used instead.', - } - ), - }, -}; - -export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ - phase, -}) => { - return ( - - {i18nTexts[phase].body} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx deleted file mode 100644 index a57a6ba4ff2c6..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx +++ /dev/null @@ -1,121 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { EuiSelect, EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui'; - -import { PhaseWithAllocationAction } from '../../../../../../common/types'; -import { propertyof } from '../../../../services/policies/policy_validation'; - -import { ErrableFormRow } from '../form_errors'; - -import { NodeAttrsDetails } from './node_attrs_details'; -import { SharedProps } from './types'; -import { LearnMoreLink } from '../learn_more_link'; - -const learnMoreLink = ( - - } - docPath="modules-cluster.html#cluster-shard-allocation-settings" - /> -); - -const i18nTexts = { - doNotModifyAllocationOption: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption', - { defaultMessage: 'Do not modify allocation configuration' } - ), -}; - -export const NodeAllocation: FunctionComponent = ({ - phase, - setPhaseData, - errors, - phaseData, - isShowingErrors, - nodes, -}) => { - const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( - null - ); - - const nodeOptions = Object.keys(nodes).map((attrs) => ({ - text: `${attrs} (${nodes[attrs].length})`, - value: attrs, - })); - - nodeOptions.sort((a, b) => a.value.localeCompare(b.value)); - - // check that this string is a valid property - const nodeAttrsProperty = propertyof('selectedNodeAttrs'); - - return ( - <> - -

- -

-
- - - {/* - TODO: this field component must be revisited to support setting multiple require values and to support - setting `include and exclude values on ILM policies. See https://github.com/elastic/kibana/issues/77344 - */} - setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} - > - - - ) : null - } - > - { - setPhaseData(nodeAttrsProperty, e.target.value); - }} - /> - - - {selectedNodeAttrsForDetails ? ( - setSelectedNodeAttrsForDetails(null)} - /> - ) : null} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx deleted file mode 100644 index c29495d13eb8e..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx +++ /dev/null @@ -1,106 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - EuiFlyoutBody, - EuiFlyout, - EuiTitle, - EuiInMemoryTable, - EuiSpacer, - EuiPortal, - EuiLoadingContent, - EuiCallOut, - EuiButton, -} from '@elastic/eui'; - -import { useLoadNodeDetails } from '../../../../services/api'; - -interface Props { - close: () => void; - selectedNodeAttrs: string; -} - -export const NodeAttrsDetails: React.FunctionComponent = ({ close, selectedNodeAttrs }) => { - const { data, isLoading, error, resendRequest } = useLoadNodeDetails(selectedNodeAttrs); - let content; - if (isLoading) { - content = ; - } else if (error) { - const { statusCode, message } = error; - content = ( - - } - color="danger" - > -

- {message} ({statusCode}) -

- - - -
- ); - } else { - content = ( - - ); - } - return ( - - - - -

- -

-
- - {content} -
-
-
- ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx deleted file mode 100644 index a7c0f3ec7c866..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx +++ /dev/null @@ -1,70 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { ListNodesRouteResponse } from '../../../../../../common/types'; -import { useLoadNodes } from '../../../../services/api'; - -interface Props { - children: (data: ListNodesRouteResponse) => JSX.Element; -} - -export const NodesDataProvider = ({ children }: Props): JSX.Element => { - const { isLoading, data, error, resendRequest } = useLoadNodes(); - - if (isLoading) { - return ( - <> - - - - ); - } - - const renderError = () => { - if (error) { - const { statusCode, message } = error; - return ( - <> - - } - color="danger" - > -

- {message} ({statusCode}) -

- - - -
- - - - ); - } - return null; - }; - - return ( - <> - {renderError()} - {/* `data` will always be defined because we use an initial value when loading */} - {children(data!)} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts deleted file mode 100644 index d3dd536d97df0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ListNodesRouteResponse, - PhaseWithAllocation, - PhaseWithAllocationAction, -} from '../../../../../../common/types'; -import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; - -export interface SharedProps { - phase: PhaseWithAllocation; - errors?: PhaseValidationErrors; - phaseData: PhaseWithAllocationAction; - setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; - isShowingErrors: boolean; - nodes: ListNodesRouteResponse['nodesByAttributes']; - hasNodeAttributes: boolean; - /** - * When on Cloud we want to disable the data tier allocation option when we detect that we are not - * using node roles in our Node config yet. See {@link ListNodesRouteResponse} for information about how this is - * detected. - */ - disableDataTierOption: boolean; -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge_legacy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge_legacy.tsx deleted file mode 100644 index 0b0dbe273c024..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge_legacy.tsx +++ /dev/null @@ -1,131 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * PLEASE NOTE: This component is currently duplicated. A version of this component wired up with - * the form lib lives in ./phases/shared - */ - -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiDescribedFormGroup, - EuiFieldNumber, - EuiFormRow, - EuiSpacer, - EuiSwitch, - EuiTextColor, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { LearnMoreLink } from './learn_more_link'; -import { ErrableFormRow } from './form_errors'; -import { Phases, PhaseWithForcemergeAction } from '../../../../../common/types'; -import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; - -const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', { - defaultMessage: 'Force merge data', -}); - -const bestCompressionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel', - { - defaultMessage: 'Compress stored fields', - } -); - -interface Props { - errors?: PhaseValidationErrors; - phase: keyof Phases & string; - phaseData: PhaseWithForcemergeAction; - setPhaseData: (dataKey: keyof PhaseWithForcemergeAction, value: boolean | string) => void; - isShowingErrors: boolean; -} - -export const Forcemerge: React.FunctionComponent = ({ - errors, - phaseData, - phase, - setPhaseData, - isShowingErrors, -}) => { - return ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth - > - { - setPhaseData('forceMergeEnabled', e.target.checked); - }} - aria-controls="forcemergeContent" - /> - - -
- {phaseData.forceMergeEnabled ? ( - <> - - { - setPhaseData('selectedForceMergeSegments', e.target.value); - }} - min={1} - /> - - - } - > - { - setPhaseData('bestCompressionEnabled', e.target.checked); - }} - /> - - - ) : null} -
-
- ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index 2b774b00b98a9..a04608338718e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -11,16 +11,7 @@ export { MinAgeInput } from './min_age_input_legacy'; export { OptionalLabel } from './optional_label'; export { PhaseErrorMessage } from './phase_error_message'; export { PolicyJsonFlyout } from './policy_json_flyout'; -export { SetPriorityInput } from './set_priority_input_legacy'; export { SnapshotPolicies } from './snapshot_policies'; -export { - DataTierAllocation, - NodeAllocation, - NodeAttrsDetails, - NodesDataProvider, - DefaultAllocationNotice, -} from './data_tier_allocation'; export { DescribedFormField } from './described_form_field'; -export { Forcemerge } from './forcemerge_legacy'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx deleted file mode 100644 index da6c358aa67c1..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx +++ /dev/null @@ -1,233 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; - -import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; - -import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../../common/types'; - -import { useFormData } from '../../../../../shared_imports'; - -import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; - -import { - LearnMoreLink, - ActiveBadge, - PhaseErrorMessage, - OptionalLabel, - ErrableFormRow, - SetPriorityInput, - MinAgeInput, - DescribedFormField, -} from '../'; - -import { DataTierAllocationFieldLegacy, useRolloverPath } from './shared'; - -const i18nTexts = { - freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', - }), - dataTierAllocation: { - description: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.description', { - defaultMessage: - 'Move data to nodes optimized for less frequent, read-only access. Store data in the cold phase on less-expensive hardware.', - }), - }, -}; - -const coldProperty: keyof Phases = 'cold'; -const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; - -interface Props { - setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; - phaseData: ColdPhaseInterface; - isShowingErrors: boolean; - errors?: PhaseValidationErrors; -} -export const ColdPhase: FunctionComponent = ({ - setPhaseData, - phaseData, - errors, - isShowingErrors, -}) => { - const [formData] = useFormData({ - watch: [useRolloverPath], - }); - - const hotPhaseRolloverEnabled = get(formData, useRolloverPath); - - return ( -
- <> - {/* Section title group; containing min age */} - -

- -

{' '} - {phaseData.phaseEnabled && !isShowingErrors ? : null} - -
- } - titleSize="s" - description={ - -

- -

- - } - id={`${coldProperty}-${phaseProperty('phaseEnabled')}`} - checked={phaseData.phaseEnabled} - onChange={(e) => { - setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); - }} - aria-controls="coldPhaseContent" - /> -
- } - fullWidth - > - {phaseData.phaseEnabled ? ( - - errors={errors} - phaseData={phaseData} - phase={coldProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - ) : null} - - {phaseData.phaseEnabled ? ( - - {/* Data tier allocation section */} - - - {/* Replicas section */} - - {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { - defaultMessage: 'Replicas', - })} - - } - description={i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', - { - defaultMessage: - 'Set the number of replicas. Remains the same as the previous phase by default.', - } - )} - switchProps={{ - label: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', - { defaultMessage: 'Set replicas' } - ), - initialValue: Boolean(phaseData.selectedReplicaCount), - onChange: (v) => { - if (!v) { - setPhaseData('selectedReplicaCount', ''); - } - }, - }} - fullWidth - > - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.selectedReplicaCount} - > - { - setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); - }} - min={0} - /> - - - {/* Freeze section */} - - - - } - description={ - - {' '} - - - } - fullWidth - titleSize="xs" - > - { - setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); - }} - label={i18nTexts.freezeLabel} - aria-label={i18nTexts.freezeLabel} - /> - - - errors={errors} - phaseData={phaseData} - phase={coldProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - /> - - ) : null} - - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx new file mode 100644 index 0000000000000..84e955a91ad7c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; + +import { EuiDescribedFormGroup, EuiTextColor } from '@elastic/eui'; + +import { Phases } from '../../../../../../../common/types'; + +import { + useFormData, + useFormContext, + UseField, + ToggleField, + NumericField, +} from '../../../../../../shared_imports'; + +import { useEditPolicyContext } from '../../../edit_policy_context'; + +import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../'; + +import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared'; + +const i18nTexts = { + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.description', { + defaultMessage: + 'Move data to nodes optimized for less frequent, read-only access. Store data in the cold phase on less-expensive hardware.', + }), + }, +}; + +const coldProperty: keyof Phases = 'cold'; + +const formFieldPaths = { + enabled: '_meta.cold.enabled', +}; + +export const ColdPhase: FunctionComponent = () => { + const { originalPolicy } = useEditPolicyContext(); + const form = useFormContext(); + + const [formData] = useFormData({ + watch: [formFieldPaths.enabled], + }); + + const enabled = get(formData, formFieldPaths.enabled); + const isShowingErrors = form.isValid === false; + + return ( +
+ <> + {/* Section title group; containing min age */} + +

+ +

{' '} + {enabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + <> +

+ +

+ + + } + fullWidth + > + {enabled && } + + {enabled && ( + <> + {/* Data tier allocation section */} + + + {/* Replicas section */} + + {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + 'data-test-subj': 'cold-setReplicasSwitch', + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean( + originalPolicy.phases.cold?.actions?.allocate?.number_of_replicas + ), + }} + fullWidth + > + + + {/* Freeze section */} + + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + > + + + + + )} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/index.ts similarity index 74% rename from x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/index.ts index fe97b85778a53..df79607f33dbc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { serializePhaseWithAllocation } from './serialize_phase_with_allocation'; +export { ColdPhase } from './cold_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx index 338e5367a1d0d..56a59270b18af 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx @@ -19,7 +19,7 @@ const i18nTexts = { 'xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription', { defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Warm nodes will be used instead.', + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', } ), }, @@ -28,7 +28,7 @@ const i18nTexts = { 'xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription', { defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Cold nodes will be used instead.', + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', } ), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx deleted file mode 100644 index d64df468620e6..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_legacy_field.tsx +++ /dev/null @@ -1,141 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiDescribedFormGroup, EuiFormRow, EuiSpacer } from '@elastic/eui'; - -import { useKibana } from '../../../../../../shared_imports'; -import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../../common/types'; -import { PhaseValidationErrors } from '../../../../../services/policies/policy_validation'; -import { getAvailableNodeRoleForPhase, isNodeRoleFirstPreference } from '../../../../../lib'; - -import { - DataTierAllocation, - DefaultAllocationNotice, - NoNodeAttributesWarning, - NodesDataProvider, - CloudDataTierCallout, -} from '../../data_tier_allocation'; - -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { - defaultMessage: 'Data allocation', - }), -}; - -interface Props { - description: React.ReactNode; - phase: PhaseWithAllocation; - setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; - isShowingErrors: boolean; - errors?: PhaseValidationErrors; - phaseData: PhaseWithAllocationAction; -} - -/** - * Top-level layout control for the data tier allocation field. - */ -export const DataTierAllocationFieldLegacy: FunctionComponent = ({ - description, - phase, - phaseData, - setPhaseData, - isShowingErrors, - errors, -}) => { - const { - services: { cloud }, - } = useKibana(); - - return ( - - {({ nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig }) => { - const hasDataNodeRoles = Object.keys(nodesByRoles).some((nodeRole) => - // match any of the "data_" roles, including data_content. - nodeRole.trim().startsWith('data_') - ); - const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); - - const renderNotice = () => { - switch (phaseData.dataTierAllocationType) { - case 'default': - const isCloudEnabled = cloud?.isCloudEnabled ?? false; - if (isCloudEnabled && phase === 'cold') { - const isUsingNodeRolesAllocation = - !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles; - const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; - - if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) { - // Tell cloud users they can deploy nodes on cloud. - return ( - <> - - - - ); - } - } - - const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); - if ( - allocationNodeRole === 'none' || - !isNodeRoleFirstPreference(phase, allocationNodeRole) - ) { - return ( - <> - - - - ); - } - break; - case 'custom': - if (!hasNodeAttrs) { - return ( - <> - - - - ); - } - break; - default: - return null; - } - }; - - return ( - {i18nTexts.title}} - description={description} - fullWidth - > - - <> - - - {/* Data tier related warnings and call-to-action notices */} - {renderNotice()} - - - - ); - }} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts index 0cae3eea6316b..6355dab89771d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts @@ -6,8 +6,6 @@ export { useRolloverPath } from '../../../constants'; -export { DataTierAllocationFieldLegacy } from './data_tier_allocation_legacy_field'; - export { DataTierAllocationField } from './data_tier_allocation_field'; export { Forcemerge } from './forcemerge_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index 7b1a4f44b5de6..06c16e8bdd5ab 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -48,16 +48,21 @@ const i18nTexts = { const warmProperty: keyof Phases = 'warm'; +const formFieldPaths = { + enabled: '_meta.warm.enabled', + warmPhaseOnRollover: '_meta.warm.warmPhaseOnRollover', +}; + export const WarmPhase: FunctionComponent = () => { const { originalPolicy } = useEditPolicyContext(); const form = useFormContext(); const [formData] = useFormData({ - watch: [useRolloverPath, '_meta.warm.enabled', '_meta.warm.warmPhaseOnRollover'], + watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover], }); - const enabled = get(formData, '_meta.warm.enabled'); + const enabled = get(formData, formFieldPaths.enabled); const hotPhaseRolloverEnabled = get(formData, useRolloverPath); - const warmPhaseOnRollover = get(formData, '_meta.warm.warmPhaseOnRollover'); + const warmPhaseOnRollover = get(formData, formFieldPaths.warmPhaseOnRollover); const isShowingErrors = form.isValid === false; return ( @@ -88,7 +93,7 @@ export const WarmPhase: FunctionComponent = () => { />

{ {hotPhaseRolloverEnabled && ( = ({ ...legacyPolicy.phases, hot: p.phases.hot, warm: p.phases.warm, + cold: p.phases.cold, }, }); } else { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input_legacy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input_legacy.tsx deleted file mode 100644 index 5efbfabdf093d..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input_legacy.tsx +++ /dev/null @@ -1,85 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * PLEASE NOTE: This component is currently duplicated. A version of this component wired up with - * the form lib lives in ./phases/shared - */ - -import React, { Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; - -import { LearnMoreLink } from './'; -import { OptionalLabel } from './'; -import { ErrableFormRow } from './'; -import { PhaseWithIndexPriority, Phases } from '../../../../../common/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; - -interface Props { - errors?: PhaseValidationErrors; - phase: keyof Phases & string; - phaseData: T; - setPhaseData: (dataKey: keyof T & string, value: any) => void; - isShowingErrors: boolean; -} -export const SetPriorityInput = ({ - errors, - phaseData, - phase, - setPhaseData, - isShowingErrors, -}: React.PropsWithChildren>) => { - const phaseIndexPriorityProperty = propertyof('phaseIndexPriority'); - return ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth - > - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.phaseIndexPriority} - > - { - setPhaseData(phaseIndexPriorityProperty, e.target.value); - }} - min={0} - /> - - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts index 760c6ad713ea0..f0294a5391d21 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts @@ -15,18 +15,27 @@ import { determineDataTierAllocationType } from '../../lib'; import { FormInternal } from './types'; export const deserializer = (policy: SerializedPolicy): FormInternal => { + const { + phases: { hot, warm, cold }, + } = policy; + const _meta: FormInternal['_meta'] = { hot: { - useRollover: Boolean(policy.phases.hot?.actions?.rollover), - forceMergeEnabled: Boolean(policy.phases.hot?.actions?.forcemerge), - bestCompression: policy.phases.hot?.actions?.forcemerge?.index_codec === 'best_compression', + useRollover: Boolean(hot?.actions?.rollover), + forceMergeEnabled: Boolean(hot?.actions?.forcemerge), + bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', }, warm: { - enabled: Boolean(policy.phases.warm), - warmPhaseOnRollover: Boolean(policy.phases.warm?.min_age === '0ms'), - forceMergeEnabled: Boolean(policy.phases.warm?.actions?.forcemerge), - bestCompression: policy.phases.warm?.actions?.forcemerge?.index_codec === 'best_compression', - dataTierAllocationType: determineDataTierAllocationType(policy.phases.warm?.actions), + enabled: Boolean(warm), + warmPhaseOnRollover: Boolean(warm?.min_age === '0ms'), + forceMergeEnabled: Boolean(warm?.actions?.forcemerge), + bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression', + dataTierAllocationType: determineDataTierAllocationType(warm?.actions), + }, + cold: { + enabled: Boolean(cold), + dataTierAllocationType: determineDataTierAllocationType(cold?.actions), + freezeEnabled: Boolean(cold?.actions?.freeze), }, }; @@ -63,6 +72,20 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { draft._meta.warm.minAgeUnit = minAge.units; } } + + if (draft.phases.cold) { + if (draft.phases.cold.actions?.allocate?.require) { + Object.entries(draft.phases.cold.actions.allocate.require).forEach((entry) => { + draft._meta.cold.allocationNodeAttribute = entry.join(':'); + }); + } + + if (draft.phases.cold.min_age) { + const minAge = splitSizeAndUnits(draft.phases.cold.min_age); + draft.phases.cold.min_age = minAge.size; + draft._meta.cold.minAgeUnit = minAge.units; + } + } } ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index eecdfb4871a67..5397f5da2d6bb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -92,6 +92,7 @@ const mergeAllSerializedPolicies = ( ...legacySerializedPolicy.phases, hot: serializedPolicy.phases.hot, warm: serializedPolicy.phases.warm, + cold: serializedPolicy.phases.cold, }, }; }; @@ -195,10 +196,6 @@ export const EditPolicy: React.FunctionComponent = ({ [setPolicy] ); - const setColdPhaseData = useCallback( - (key: string, value: any) => setPhaseData('cold', key, value), - [setPhaseData] - ); const setDeletePhaseData = useCallback( (key: string, value: any) => setPhaseData('delete', key, value), [setPhaseData] @@ -342,14 +339,7 @@ export const EditPolicy: React.FunctionComponent = ({ - 0 - } - setPhaseData={setColdPhaseData} - phaseData={policy.phases.cold} - /> + diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts index a80382e87539c..070f03f74b954 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts @@ -11,7 +11,11 @@ import { defaultSetPriority, defaultPhaseIndexPriority } from '../../constants'; import { FormInternal } from './types'; -import { ifExistsNumberGreaterThanZero, rolloverThresholdsValidator } from './form_validations'; +import { + ifExistsNumberGreaterThanZero, + ifExistsNumberNonNegative, + rolloverThresholdsValidator, +} from './form_validations'; import { i18nTexts } from './i18n_texts'; @@ -69,6 +73,30 @@ export const schema: FormSchema = { label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, }, }, + cold: { + enabled: { + defaultValue: false, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel', + { defaultMessage: 'Activate cold phase' } + ), + }, + freezeEnabled: { + defaultValue: false, + label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + }, + minAgeUnit: { + defaultValue: 'd', + }, + dataTierAllocationType: { + label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel, + }, + allocationNodeAttribute: { + label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, + }, + }, }, phases: { hot: { @@ -138,7 +166,7 @@ export const schema: FormSchema = { priority: { defaultValue: defaultSetPriority as any, label: i18nTexts.editPolicy.setPriorityFieldLabel, - validations: [{ validator: ifExistsNumberGreaterThanZero }], + validations: [{ validator: ifExistsNumberNonNegative }], serializer: serializers.stringToNumber, }, }, @@ -217,7 +245,48 @@ export const schema: FormSchema = { priority: { defaultValue: defaultPhaseIndexPriority as any, label: i18nTexts.editPolicy.setPriorityFieldLabel, - validations: [{ validator: ifExistsNumberGreaterThanZero }], + validations: [{ validator: ifExistsNumberNonNegative }], + serializer: serializers.stringToNumber, + }, + }, + }, + }, + cold: { + min_age: { + defaultValue: '0', + validations: [ + { + validator: (arg) => + numberGreaterThanField({ + than: 0, + allowEquality: true, + message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired, + })({ + ...arg, + value: arg.value === '' ? -Infinity : parseInt(arg.value, 10), + }), + }, + ], + }, + actions: { + allocate: { + number_of_replicas: { + label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel', { + defaultMessage: 'Number of replicas (optional)', + }), + validations: [ + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: serializers.stringToNumber, + }, + }, + set_priority: { + priority: { + defaultValue: '0' as any, + label: i18nTexts.editPolicy.setPriorityFieldLabel, + validations: [{ validator: ifExistsNumberNonNegative }], serializer: serializers.stringToNumber, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts index 37ca4e9def340..9c855ccb41624 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts @@ -12,18 +12,36 @@ import { i18nTexts } from './i18n_texts'; const { numberGreaterThanField } = fieldValidators; -export const ifExistsNumberGreaterThanZero: ValidationFunc = (arg) => { - if (arg.value) { - return numberGreaterThanField({ - than: 0, - message: i18nTexts.editPolicy.errors.numberGreatThan0Required, - })({ - ...arg, - value: parseInt(arg.value, 10), - }); - } +const createIfNumberExistsValidator = ({ + than, + message, +}: { + than: number; + message: string; +}): ValidationFunc => { + return (arg) => { + if (arg.value) { + return numberGreaterThanField({ + than, + message, + })({ + ...arg, + value: parseInt(arg.value, 10), + }); + } + }; }; +export const ifExistsNumberGreaterThanZero = createIfNumberExistsValidator({ + than: 0, + message: i18nTexts.editPolicy.errors.numberGreatThan0Required, +}); + +export const ifExistsNumberNonNegative = createIfNumberExistsValidator({ + than: -1, + message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired, +}); + /** * A special validation type used to keep track of validation errors for * the rollover threshold values not being set (e.g., age and doc count) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts index 90e81528f5afe..564b5a2c4e397 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts @@ -31,6 +31,11 @@ const serializeAllocateAction = ( [name]: value, }, }; + } else { + // The form has been configured to use node attribute based allocation but no node attribute + // was selected. We fall back to what was originally selected in this case. This might be + // migrate.enabled: "false" + actions.migrate = originalActions.migrate; } // copy over the original include and exclude values until we can set them in the form. @@ -129,5 +134,36 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( } } + /** + * COLD PHASE SERIALIZATION + */ + if (policy.phases.cold) { + if (policy.phases.cold.min_age) { + policy.phases.cold.min_age = `${policy.phases.cold.min_age}${_meta.cold.minAgeUnit}`; + } + + policy.phases.cold.actions = serializeAllocateAction( + _meta.cold, + policy.phases.cold.actions, + originalPolicy?.phases.cold?.actions + ); + + if ( + policy.phases.cold.actions.allocate && + !policy.phases.cold.actions.allocate.require && + !isNumber(policy.phases.cold.actions.allocate.number_of_replicas) && + isEmpty(policy.phases.cold.actions.allocate.include) && + isEmpty(policy.phases.cold.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete policy.phases.cold.actions.allocate; + } + + if (_meta.cold.freezeEnabled) { + policy.phases.cold.actions.freeze = {}; + } + } + return policy; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 6fcfbd050c69d..1884f8dbc0619 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -13,20 +13,29 @@ export interface DataAllocationMetaFields { allocationNodeAttribute?: string; } -interface HotPhaseMetaFields { - useRollover: boolean; +export interface MinAgeField { + minAgeUnit?: string; +} + +export interface ForcemergeFields { forceMergeEnabled: boolean; bestCompression: boolean; +} + +interface HotPhaseMetaFields extends ForcemergeFields { + useRollover: boolean; maxStorageSizeUnit?: string; maxAgeUnit?: string; } -interface WarmPhaseMetaFields extends DataAllocationMetaFields { +interface WarmPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, ForcemergeFields { enabled: boolean; - forceMergeEnabled: boolean; - bestCompression: boolean; warmPhaseOnRollover: boolean; - minAgeUnit?: string; +} + +interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { + enabled: boolean; + freezeEnabled: boolean; } /** @@ -40,5 +49,6 @@ export interface FormInternal extends SerializedPolicy { _meta: { hot: HotPhaseMetaFields; warm: WarmPhaseMetaFields; + cold: ColdPhaseMetaFields; }; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts deleted file mode 100644 index faf3954f93fd8..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ /dev/null @@ -1,154 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash'; -import { AllocateAction, ColdPhase, SerializedColdPhase } from '../../../../common/types'; -import { serializedPhaseInitialization } from '../../constants'; -import { isNumber, splitSizeAndUnits } from './policy_serialization'; -import { - numberRequiredMessage, - PhaseValidationErrors, - positiveNumberRequiredMessage, -} from './policy_validation'; -import { determineDataTierAllocationTypeLegacy } from '../../lib'; -import { serializePhaseWithAllocation } from './shared'; - -export const coldPhaseInitialization: ColdPhase = { - phaseEnabled: false, - selectedMinimumAge: '0', - selectedMinimumAgeUnits: 'd', - selectedNodeAttrs: '', - selectedReplicaCount: '', - freezeEnabled: false, - phaseIndexPriority: '', - dataTierAllocationType: 'default', -}; - -export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => { - const phase = { ...coldPhaseInitialization }; - if (phaseSerialized === undefined || phaseSerialized === null) { - return phase; - } - - phase.phaseEnabled = true; - - if (phaseSerialized.actions) { - phase.dataTierAllocationType = determineDataTierAllocationTypeLegacy(phaseSerialized.actions); - } - - if (phaseSerialized.min_age) { - const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); - phase.selectedMinimumAge = minAge; - phase.selectedMinimumAgeUnits = minAgeUnits; - } - - if (phaseSerialized.actions) { - const actions = phaseSerialized.actions; - if (actions.allocate) { - const allocate = actions.allocate; - if (allocate.require) { - Object.entries(allocate.require).forEach((entry) => { - phase.selectedNodeAttrs = entry.join(':'); - }); - if (allocate.number_of_replicas) { - phase.selectedReplicaCount = allocate.number_of_replicas.toString(); - } - } - } - - if (actions.freeze) { - phase.freezeEnabled = true; - } - - if (actions.set_priority) { - phase.phaseIndexPriority = actions.set_priority.priority - ? actions.set_priority.priority.toString() - : ''; - } - } - - return phase; -}; - -export const coldPhaseToES = ( - phase: ColdPhase, - originalPhase: SerializedColdPhase | undefined -): SerializedColdPhase => { - if (!originalPhase) { - originalPhase = { ...serializedPhaseInitialization }; - } - - const esPhase = { ...originalPhase }; - - if (isNumber(phase.selectedMinimumAge)) { - esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; - } - - esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); - - if (isNumber(phase.selectedReplicaCount)) { - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.number_of_replicas; - } - } - - if ( - esPhase.actions.allocate && - !esPhase.actions.allocate.require && - !isNumber(esPhase.actions.allocate.number_of_replicas) && - isEmpty(esPhase.actions.allocate.include) && - isEmpty(esPhase.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete esPhase.actions.allocate; - } - - if (phase.freezeEnabled) { - esPhase.actions.freeze = {}; - } else { - delete esPhase.actions.freeze; - } - - if (isNumber(phase.phaseIndexPriority)) { - esPhase.actions.set_priority = { - priority: parseInt(phase.phaseIndexPriority, 10), - }; - } else { - delete esPhase.actions.set_priority; - } - - return esPhase; -}; - -export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors => { - if (!phase.phaseEnabled) { - return {}; - } - - const phaseErrors = {} as PhaseValidationErrors; - - // index priority is optional, but if it's set, it needs to be a positive number - if (phase.phaseIndexPriority) { - if (!isNumber(phase.phaseIndexPriority)) { - phaseErrors.phaseIndexPriority = [numberRequiredMessage]; - } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { - phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; - } - } - - // min age needs to be a positive number - if (!isNumber(phase.selectedMinimumAge)) { - phaseErrors.selectedMinimumAge = [numberRequiredMessage]; - } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { - phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; - } - - return { ...phaseErrors }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts index 0be6ab3521736..19481b39a2c80 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts @@ -7,9 +7,7 @@ // eslint-disable-next-line no-restricted-imports import cloneDeep from 'lodash/cloneDeep'; import { deserializePolicy, legacySerializePolicy } from './policy_serialization'; -import { defaultNewColdPhase, defaultNewDeletePhase } from '../../constants'; -import { DataTierAllocationType } from '../../../../common/types'; -import { coldPhaseInitialization } from './cold_phase'; +import { defaultNewDeletePhase } from '../../constants'; describe('Policy serialization', () => { test('serialize a policy using "default" data allocation', () => { @@ -18,12 +16,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'default', - selectedNodeAttrs: 'another:thing', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -31,24 +23,12 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, }, } ) ).toEqual({ name: 'test', - phases: { - cold: { - actions: { - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); @@ -58,12 +38,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'custom', - selectedNodeAttrs: 'another:thing', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -71,37 +45,12 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - cold: { - actions: { - allocate: { - include: { keep: 'this' }, - exclude: { keep: 'this' }, - require: { something: 'here' }, - }, - }, - }, }, } ) ).toEqual({ name: 'test', - phases: { - cold: { - actions: { - allocate: { - include: { keep: 'this' }, - exclude: { keep: 'this' }, - require: { - another: 'thing', - }, - }, - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); @@ -111,12 +60,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'custom', - selectedNodeAttrs: '', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -124,26 +67,13 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, }, } ) ).toEqual({ // There should be no allocation action in any phases... name: 'test', - phases: { - cold: { - actions: { - allocate: { include: {}, exclude: {}, require: { something: 'here' } }, - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); @@ -153,12 +83,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'none', - selectedNodeAttrs: 'ignore:this', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -166,39 +90,20 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, }, } ) ).toEqual({ // There should be no allocation action in any phases... name: 'test', - phases: { - cold: { - actions: { - migrate: { - enabled: false, - }, - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); test('serialization does not alter the original policy', () => { const originalPolicy = { name: 'test', - phases: { - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, - }, + phases: {}, }; const originalClone = cloneDeep(originalPolicy); @@ -206,13 +111,6 @@ describe('Policy serialization', () => { const deserializedPolicy = { name: 'test', phases: { - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'none' as DataTierAllocationType, - selectedNodeAttrs: 'ignore:this', - phaseEnabled: true, - }, - delete: { ...defaultNewDeletePhase }, }, }; @@ -227,9 +125,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - cold: { - ...defaultNewColdPhase, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -276,9 +171,6 @@ describe('Policy serialization', () => { ).toEqual({ name: 'test', phases: { - cold: { - ...coldPhaseInitialization, - }, delete: { ...defaultNewDeletePhase }, }, }); @@ -290,9 +182,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - cold: { - ...defaultNewColdPhase, - }, delete: { ...defaultNewDeletePhase }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 32c7e698b0920..55e9d88dcd383 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -6,13 +6,8 @@ import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types'; -import { - defaultNewColdPhase, - defaultNewDeletePhase, - serializedPhaseInitialization, -} from '../../constants'; +import { defaultNewDeletePhase, serializedPhaseInitialization } from '../../constants'; -import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; export const splitSizeAndUnits = (field: string): { size: string; units: string } => { @@ -46,7 +41,6 @@ export const initializeNewPolicy = (newPolicyName: string = ''): LegacyPolicy => return { name: newPolicyName, phases: { - cold: { ...defaultNewColdPhase }, delete: { ...defaultNewDeletePhase }, }, }; @@ -61,7 +55,6 @@ export const deserializePolicy = (policy: PolicyFromES): LegacyPolicy => { return { name, phases: { - cold: coldPhaseFromES(phases.cold), delete: deletePhaseFromES(phases.delete), }, }; @@ -79,10 +72,6 @@ export const legacySerializePolicy = ( phases: {}, } as SerializedPolicy; - if (policy.phases.cold.phaseEnabled) { - serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); - } - if (policy.phases.delete.phaseEnabled) { serializedPolicy.phases.delete = deletePhaseToES( policy.phases.delete, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index a113cb68a2349..79c909c433f33 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -5,8 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ColdPhase, DeletePhase, LegacyPolicy, PolicyFromES } from '../../../../common/types'; -import { validateColdPhase } from './cold_phase'; +import { DeletePhase, LegacyPolicy, PolicyFromES } from '../../../../common/types'; import { validateDeletePhase } from './delete_phase'; export const propertyof = (propertyName: keyof T & string) => propertyName; @@ -82,7 +81,6 @@ export type PhaseValidationErrors = { }; export interface ValidationErrors { - cold: PhaseValidationErrors; delete: PhaseValidationErrors; policyName: string[]; } @@ -120,17 +118,12 @@ export const validatePolicy = ( } } - const coldPhaseErrors = validateColdPhase(policy.phases.cold); const deletePhaseErrors = validateDeletePhase(policy.phases.delete); - const isValid = - policyNameErrors.length === 0 && - Object.keys(coldPhaseErrors).length === 0 && - Object.keys(deletePhaseErrors).length === 0; + const isValid = policyNameErrors.length === 0 && Object.keys(deletePhaseErrors).length === 0; return [ isValid, { policyName: [...policyNameErrors], - cold: coldPhaseErrors, delete: deletePhaseErrors, }, ]; @@ -145,9 +138,6 @@ export const findFirstError = (errors?: ValidationErrors): string | undefined => return propertyof('policyName'); } - if (Object.keys(errors.cold).length > 0) { - return `${propertyof('cold')}.${Object.keys(errors.cold)[0]}`; - } if (Object.keys(errors.delete).length > 0) { return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts deleted file mode 100644 index c29ae3ab22831..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts +++ /dev/null @@ -1,42 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import cloneDeep from 'lodash/cloneDeep'; - -import { - AllocateAction, - PhaseWithAllocationAction, - SerializedPhase, -} from '../../../../../common/types'; - -export const serializePhaseWithAllocation = ( - phase: PhaseWithAllocationAction, - originalPhaseActions: SerializedPhase['actions'] = {} -): SerializedPhase['actions'] => { - const esPhaseActions: SerializedPhase['actions'] = cloneDeep(originalPhaseActions); - - if (phase.dataTierAllocationType === 'custom' && phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhaseActions.allocate = esPhaseActions.allocate || ({} as AllocateAction); - esPhaseActions.allocate.require = { - [name]: value, - }; - } else if (phase.dataTierAllocationType === 'none') { - esPhaseActions.migrate = { enabled: false }; - if (esPhaseActions.allocate) { - delete esPhaseActions.allocate; - } - } else if (phase.dataTierAllocationType === 'default') { - if (esPhaseActions.allocate) { - delete esPhaseActions.allocate.require; - } - delete esPhaseActions.migrate; - } - - return esPhaseActions; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index c77e3d22f0e37..2f1c7798e7a4d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -9,7 +9,6 @@ import { UIM_CONFIG_WARM_PHASE, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, - defaultNewColdPhase, defaultPhaseIndexPriority, } from '../constants/'; @@ -23,7 +22,7 @@ describe('getUiMetricsForPhases', () => { min_age: '0ms', actions: { set_priority: { - priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), + priority: parseInt(defaultPhaseIndexPriority, 10), }, }, }, @@ -69,7 +68,7 @@ describe('getUiMetricsForPhases', () => { actions: { freeze: {}, set_priority: { - priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), + priority: parseInt(defaultPhaseIndexPriority, 10), }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index 305b35b23e4d8..274d3d1ca97f3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -13,7 +13,6 @@ import { UIM_CONFIG_FREEZE_INDEX, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_WARM_PHASE, - defaultNewColdPhase, defaultSetPriority, defaultPhaseIndexPriority, } from '../constants'; @@ -55,8 +54,7 @@ export function getUiMetricsForPhases(phases: Phases): string[] { const isColdPhasePriorityChanged = phases.cold && phases.cold.actions.set_priority && - phases.cold.actions.set_priority.priority !== - parseInt(defaultNewColdPhase.phaseIndexPriority, 10); + phases.cold.actions.set_priority.priority !== parseInt(defaultPhaseIndexPriority, 10); // If the priority is different than the default, we'll consider it a user interaction, // even if the user has set it to undefined. return ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d953a47620000..58ee36507cb73 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9079,7 +9079,6 @@ "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "インデックスを読み取り専用にし、メモリー消費量を最小化します。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "凍結", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel": "レプリカを設定", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel": "データティアオプション", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "ノード属性に基づいてデータを移動します。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "カスタム", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "コールドティアのノードにデータを移動します。", @@ -9143,7 +9142,6 @@ "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名前", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "ノード属性を使用して、シャード割り当てを制御します。{learnMoreLink}。", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "割り当て構成を修正しない", - "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "ノード属性を選択", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "ノード属性を読み込めません", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "カスタムノード属性が構成されていません", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "再試行", @@ -9261,7 +9259,6 @@ "xpack.indexLifecycleMgmt.indexMgmtFilter.managedLabel": "管理中", "xpack.indexLifecycleMgmt.indexMgmtFilter.unmanagedLabel": "管理対象外", "xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel": "ウォーム", - "xpack.indexLifecycleMgmt.indexPriorityLabel": "インデックスの優先順位", "xpack.indexLifecycleMgmt.learnMore": "その他のリソース", "xpack.indexLifecycleMgmt.licenseCheckErrorMessage": "ライセンス確認失敗", "xpack.indexLifecycleMgmt.nodeAttrDetails.hostField": "ホスト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 98e79e784e880..832b716934bca 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9088,7 +9088,6 @@ "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "使索引只读,并最大限度减小其内存占用。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "冻结", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel": "设置副本", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel": "数据层选项", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "根据节点属性移动数据。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "定制", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "将数据移到冷层中的节点。", @@ -9152,7 +9151,6 @@ "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名称", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "使用节点属性控制分片分配。{learnMoreLink}。", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "不要修改分配配置", - "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "选择节点属性", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "无法加载节点属性", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "未配置定制节点属性", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "重试", @@ -9270,7 +9268,6 @@ "xpack.indexLifecycleMgmt.indexMgmtFilter.managedLabel": "受管", "xpack.indexLifecycleMgmt.indexMgmtFilter.unmanagedLabel": "未受管", "xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel": "温", - "xpack.indexLifecycleMgmt.indexPriorityLabel": "索引优先级", "xpack.indexLifecycleMgmt.learnMore": "了解详情", "xpack.indexLifecycleMgmt.licenseCheckErrorMessage": "许可证检查失败", "xpack.indexLifecycleMgmt.nodeAttrDetails.hostField": "主机", From eb30e9513dc481008e7f2a384b4c0349c79dfabc Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 28 Oct 2020 09:23:35 -0700 Subject: [PATCH 04/73] [Enterprise Search] Add missing Enterprise Search Overview telemetry collectors/schema (#81858) * Add Enterprise Search cannot connect telemetry event * Add missing server-side telemetry collectors & schema --- .../error_connecting/error_connecting.tsx | 2 ++ .../enterprise_search/telemetry.test.ts | 10 ++++++++++ .../collectors/enterprise_search/telemetry.ts | 16 ++++++++++++++++ .../schema/xpack_plugins.json | 10 ++++++++++ 4 files changed, 38 insertions(+) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx index 567c77792583d..7af3a1d1272e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { EuiPage, EuiPageContent } from '@elastic/eui'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { ErrorStatePrompt } from '../../../shared/error_state'; export const ErrorConnecting: React.FC = () => ( + diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts index c3e2aff6551c9..440f540ccc857 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts @@ -20,6 +20,8 @@ describe('Enterprise Search Telemetry Usage Collector', () => { get: () => ({ attributes: { 'ui_viewed.overview': 10, + 'ui_viewed.setup_guide': 5, + 'ui_error.cannot_connect': 1, 'ui_clicked.app_search': 2, 'ui_clicked.workplace_search': 3, }, @@ -53,6 +55,10 @@ describe('Enterprise Search Telemetry Usage Collector', () => { expect(savedObjectsCounts).toEqual({ ui_viewed: { overview: 10, + setup_guide: 5, + }, + ui_error: { + cannot_connect: 1, }, ui_clicked: { app_search: 2, @@ -74,6 +80,10 @@ describe('Enterprise Search Telemetry Usage Collector', () => { expect(savedObjectsCounts).toEqual({ ui_viewed: { overview: 0, + setup_guide: 0, + }, + ui_error: { + cannot_connect: 0, }, ui_clicked: { app_search: 0, diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts index a124a185b9a34..d6bd8bd9305f5 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts @@ -13,6 +13,10 @@ import { getSavedObjectAttributesFromRepo } from '../lib/telemetry'; interface ITelemetry { ui_viewed: { overview: number; + setup_guide: number; + }; + ui_error: { + cannot_connect: number; }; ui_clicked: { app_search: number; @@ -38,6 +42,10 @@ export const registerTelemetryUsageCollector = ( schema: { ui_viewed: { overview: { type: 'long' }, + setup_guide: { type: 'long' }, + }, + ui_error: { + cannot_connect: { type: 'long' }, }, ui_clicked: { app_search: { type: 'long' }, @@ -63,6 +71,10 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log const defaultTelemetrySavedObject: ITelemetry = { ui_viewed: { overview: 0, + setup_guide: 0, + }, + ui_error: { + cannot_connect: 0, }, ui_clicked: { app_search: 0, @@ -78,6 +90,10 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log return { ui_viewed: { overview: get(savedObjectAttributes, 'ui_viewed.overview', 0), + setup_guide: get(savedObjectAttributes, 'ui_viewed.setup_guide', 0), + }, + ui_error: { + cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0), }, ui_clicked: { app_search: get(savedObjectAttributes, 'ui_clicked.app_search', 0), diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 396a06205eaa9..2b3ff6c8a0aef 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1706,6 +1706,16 @@ "properties": { "overview": { "type": "long" + }, + "setup_guide": { + "type": "long" + } + } + }, + "ui_error": { + "properties": { + "cannot_connect": { + "type": "long" } } }, From d7a78229e4691a09316c42734ed201e9c66571d9 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Wed, 28 Oct 2020 17:35:56 +0100 Subject: [PATCH 05/73] [Security Solution] Unskips Alerts and Persistent timeline cypress tests (#81898) * unskips alerts tests * unskips persistent timeline tests --- .../security_solution/cypress/integration/alerts.spec.ts | 3 +-- .../cypress/integration/timeline_local_storage.spec.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index 07d0d63e57059..db841d2a732c4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,8 +30,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/77957 -describe.skip('Alerts', () => { +describe('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index c2ff2c58687f3..383ebe2220585 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,8 +13,7 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; import { removeColumn, resetFields } from '../tasks/timeline'; -// FLAKY: https://github.com/elastic/kibana/issues/75794 -describe.skip('persistent timeline', () => { +describe('persistent timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); From 6272f4e0fdb0986f2e28e407cae0aa4a2cbafc2e Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 28 Oct 2020 13:03:36 -0500 Subject: [PATCH 06/73] [Metrics UI] Fix a Chrome bug with Inventory View flickering at certain sizes (#81514) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/metrics/inventory_view/components/waffle/map.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx index 6621b110a6dfd..8023e3bf7da62 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx @@ -45,8 +45,8 @@ export const Map: React.FC = ({ const sortedNodes = sortNodes(options.sort, nodes); const map = nodesToWaffleMap(sortedNodes); return ( - - {({ measureRef, content: { width = 0, height = 0 } }) => { + + {({ measureRef, bounds: { width = 0, height = 0 } }) => { const groupsWithLayout = applyWaffleMapLayout(map, width, height); return ( Date: Wed, 28 Oct 2020 14:21:41 -0400 Subject: [PATCH 07/73] Fixing flaky test (#81901) --- .../test_suites/task_manager/task_management.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts index 348ff35b2968f..f34cb7594d288 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts @@ -57,8 +57,7 @@ export default function ({ getService }: FtrProviderContext) { const testHistoryIndex = '.kibana_task_manager_test_result'; const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); - // Failing: See https://github.com/elastic/kibana/issues/81853 - describe.skip('scheduling and running tasks', () => { + describe('scheduling and running tasks', () => { beforeEach( async () => await supertest.delete('/api/sample_tasks').set('kbn-xsrf', 'xxx').expect(200) ); @@ -673,7 +672,7 @@ export default function ({ getService }: FtrProviderContext) { const [scheduledTask] = (await currentTasks()).docs; expect(scheduledTask.id).to.eql(task.id); expect(scheduledTask.status).to.eql('claiming'); - expect(scheduledTask.attempts).to.eql(4); + expect(scheduledTask.attempts).to.be.greaterThan(3); }); }); }); From 2a4337e8b50a445016084c9f120ef0d81a6c0d4a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 28 Oct 2020 19:33:30 +0100 Subject: [PATCH 08/73] [UX] Fix core vitals empty state (#81781) --- .../RumDashboard/UXMetrics/KeyUXMetrics.tsx | 9 +- .../UXMetrics/__tests__/KeyUXMetrics.test.tsx | 1 + .../app/RumDashboard/UXMetrics/index.tsx | 14 +- .../app/RumDashboard/UserPercentile/index.tsx | 2 - .../__snapshots__/queries.test.ts.snap | 12 +- .../lib/rum_client/get_web_core_vitals.ts | 61 ++++---- .../app/section/ux/mock_data/ux.mock.ts | 1 + .../core_web_vitals/core_vital_item.tsx | 6 +- .../shared/core_web_vitals/index.tsx | 37 +++-- .../core_web_vitals/web_core_vitals_title.tsx | 134 ++++++++++++++---- .../observability/public/data_handler.test.ts | 2 + .../trial/tests/csm/web_core_vitals.ts | 21 ++- 12 files changed, 209 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index 793c9619edb3d..e91f129195366 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -37,8 +37,9 @@ interface Props { loading: boolean; } -function formatTitle(unit: string, value?: number) { - if (typeof value === 'undefined') return DATA_UNDEFINED_LABEL; +function formatTitle(unit: string, value?: number | null) { + if (typeof value === 'undefined' || value === null) + return DATA_UNDEFINED_LABEL; return formatToSec(value, unit); } @@ -85,8 +86,8 @@ export function KeyUXMetrics({ data, loading }: Props) { { lcpRanks: [69, 17, 14], fidRanks: [83, 6, 11], clsRanks: [90, 7, 3], + coreVitalPages: 1000, }} /> ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 521cb6cdf116f..983e3be1c21a9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -18,6 +18,7 @@ import { KeyUXMetrics } from './KeyUXMetrics'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUxQuery } from '../hooks/useUxQuery'; import { CoreVitals } from '../../../../../../observability/public'; +import { CsmSharedContext } from '../CsmSharedContext'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { getPercentileLabel } from './translations'; @@ -43,6 +44,10 @@ export function UXMetrics() { [uxQuery] ); + const { + sharedData: { totalPageViews }, + } = useContext(CsmSharedContext); + return ( @@ -62,7 +67,12 @@ export function UXMetrics() { - + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx index 18cd7d79cc69f..04c7e3cc00287 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx @@ -45,13 +45,11 @@ export function UserPercentile() { { value: '50', text: I18LABELS.percentile50thMedian, - dropdownDisplay: I18LABELS.percentile50thMedian, 'data-test-subj': 'p50Percentile', }, { value: '75', text: I18LABELS.percentile75th, - dropdownDisplay: I18LABELS.percentile75th, 'data-test-subj': 'p75Percentile', }, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index eedc3a83cd376..53dcd2f469148 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -580,6 +580,13 @@ Object { ], }, }, + "coreVitalPages": Object { + "filter": Object { + "exists": Object { + "field": "transaction.experience", + }, + }, + }, "fcp": Object { "percentiles": Object { "field": "transaction.marks.agent.firstContentfulPaint", @@ -660,11 +667,6 @@ Object { "service.environment": "test", }, }, - Object { - "term": Object { - "user_agent.name": "Chrome", - }, - }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index c5baf0b529eb4..76a718bbb2a02 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -12,7 +12,6 @@ import { FCP_FIELD, FID_FIELD, LCP_FIELD, - USER_AGENT_NAME, TBT_FIELD, } from '../../../common/elasticsearch_fieldnames'; @@ -35,17 +34,17 @@ export async function getWebCoreVitals({ size: 0, query: { bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [USER_AGENT_NAME]: 'Chrome', - }, - }, - ], + filter: [...projection.body.query.bool.filter], }, }, aggs: { + coreVitalPages: { + filter: { + exists: { + field: 'transaction.experience', + }, + }, + }, lcp: { percentiles: { field: LCP_FIELD, @@ -104,13 +103,22 @@ export async function getWebCoreVitals({ const { apmEventClient } = setup; const response = await apmEventClient.search(params); - const { lcp, cls, fid, tbt, fcp, lcpRanks, fidRanks, clsRanks } = - response.aggregations ?? {}; + const { + lcp, + cls, + fid, + tbt, + fcp, + lcpRanks, + fidRanks, + clsRanks, + coreVitalPages, + } = response.aggregations ?? {}; const getRanksPercentages = ( - ranks: Array<{ key: number; value: number }> + ranks?: Array<{ key: number; value: number }> ) => { - const ranksVal = ranks.map(({ value }) => value?.toFixed(0) ?? 0); + const ranksVal = ranks?.map(({ value }) => value?.toFixed(0) ?? 0) ?? []; return [ Number(ranksVal?.[0]), Number(ranksVal?.[1]) - Number(ranksVal?.[0]), @@ -118,23 +126,26 @@ export async function getWebCoreVitals({ ]; }; - const defaultRanks = [ - { value: 0, key: 0 }, - { value: 0, key: 0 }, - ]; + const defaultRanks = [100, 0, 0]; const pkey = percentile.toFixed(1); - // Divide by 1000 to convert ms into seconds return { - cls: String(cls?.values[pkey]?.toFixed(2) || 0), - fid: fid?.values[pkey] ?? 0, - lcp: lcp?.values[pkey] ?? 0, + coreVitalPages: coreVitalPages?.doc_count ?? 0, + cls: cls?.values[pkey]?.toFixed(3) || null, + fid: fid?.values[pkey], + lcp: lcp?.values[pkey], tbt: tbt?.values[pkey] ?? 0, - fcp: fcp?.values[pkey] ?? 0, + fcp: fcp?.values[pkey], - lcpRanks: getRanksPercentages(lcpRanks?.values ?? defaultRanks), - fidRanks: getRanksPercentages(fidRanks?.values ?? defaultRanks), - clsRanks: getRanksPercentages(clsRanks?.values ?? defaultRanks), + lcpRanks: lcp?.values[pkey] + ? getRanksPercentages(lcpRanks?.values) + : defaultRanks, + fidRanks: fid?.values[pkey] + ? getRanksPercentages(fidRanks?.values) + : defaultRanks, + clsRanks: cls?.values[pkey] + ? getRanksPercentages(clsRanks?.values) + : defaultRanks, }; } diff --git a/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts b/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts index e61564f9df753..017f385d36735 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts +++ b/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts @@ -14,6 +14,7 @@ export const response: UxFetchDataResponse = { lcp: 1942.6666666666667, tbt: 281.55833333333334, fcp: 1487, + coreVitalPages: 100, lcpRanks: [65, 19, 16], fidRanks: [73, 11, 16], clsRanks: [86, 8, 6], diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx index 0d0a388855ff2..18831565b8784 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx @@ -34,7 +34,7 @@ export interface Thresholds { interface Props { title: string; - value?: string; + value?: string | null; ranks?: number[]; loading: boolean; thresholds: Thresholds; @@ -88,14 +88,14 @@ export function CoreVitalItem({ const biggestValIndex = ranks.indexOf(Math.max(...ranks)); - if (value === undefined && ranks[0] === 100 && !loading) { + if ((value === null || value !== undefined) && ranks[0] === 100 && !loading) { return ; } return ( <> {title} diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx index 6a507176e55f6..f5683310c3b7c 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx @@ -18,11 +18,12 @@ import { WebCoreVitalsTitle } from './web_core_vitals_title'; import { ServiceName } from './service_name'; export interface UXMetrics { - cls: string; - fid: number; - lcp: number; + cls: string | null; + fid?: number | null; + lcp?: number | null; tbt: number; - fcp: number; + fcp?: number | null; + coreVitalPages: number; lcpRanks: number[]; fidRanks: number[]; clsRanks: number[]; @@ -48,21 +49,35 @@ interface Props { data?: UXMetrics | null; displayServiceName?: boolean; serviceName?: string; + totalPageViews?: number; + displayTrafficMetric?: boolean; } -function formatValue(value?: number) { - if (typeof value === 'undefined') { - return undefined; +function formatValue(value?: number | null) { + if (typeof value === 'undefined' || value === null) { + return null; } return formatToSec(value, 'ms'); } -export function CoreVitals({ data, loading, displayServiceName, serviceName }: Props) { - const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {}; +export function CoreVitals({ + data, + loading, + displayServiceName, + serviceName, + totalPageViews, + displayTrafficMetric = false, +}: Props) { + const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks, coreVitalPages } = data || {}; return ( <> - + {displayServiceName && } @@ -90,7 +105,7 @@ export function CoreVitals({ data, loading, displayServiceName, serviceName }: P setIsPopoverOpen(false); + const closeBrowserPopover = () => setIsBrowserPopoverOpen(false); return ( - -

- {CORE_WEB_VITALS} - setIsPopoverOpen(true)} - color={'text'} - iconType={'questionInCircle'} - /> - } - closePopover={closePopover} - > -
- + + + +

+ {CORE_WEB_VITALS} + setIsPopoverOpen(true)} + color={'text'} + iconType={'questionInCircle'} + /> + } + closePopover={closePopover} + > +
+ + {' '} + + {CORE_WEB_VITALS} + + +
+
+

+
+
+ {displayTrafficMetric && totalPageViews > 0 && ( + + {loading ? ( + + ) : ( + {(((coreVitalPages || 0) / totalPageViews) * 100).toFixed(0)}% + ), + }} /> - - {' '} - {CORE_WEB_VITALS} - + + setIsBrowserPopoverOpen(true)} + color={'text'} + iconType={'questionInCircle'} + /> + } + closePopover={closeBrowserPopover} + > +
+ + {' '} + + {BROWSER_CORE_WEB_VITALS} + + +
+
-
-
-

-
+ )} +
+ )} + ); } diff --git a/x-pack/plugins/observability/public/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts index dae2f62777d30..8fdfc2bc622ca 100644 --- a/x-pack/plugins/observability/public/data_handler.test.ts +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -287,6 +287,7 @@ describe('registerDataHandler', () => { lcp: 1464.3333333333333, tbt: 232.92166666666665, fcp: 1154.8, + coreVitalPages: 100, lcpRanks: [73, 16, 11], fidRanks: [85, 4, 11], clsRanks: [88, 7, 5], @@ -314,6 +315,7 @@ describe('registerDataHandler', () => { lcp: 1464.3333333333333, tbt: 232.92166666666665, fcp: 1154.8, + coreVitalPages: 100, lcpRanks: [73, 16, 11], fidRanks: [85, 4, 11], clsRanks: [88, 7, 5], diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index efbdb75c47cc1..5dbe266deeb81 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts @@ -21,14 +21,12 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expect(response.status).to.be(200); expect(response.body).to.eql({ - cls: '0', - fid: '0.00', - lcp: '0.00', - tbt: '0.00', - fcp: 0, - lcpRanks: [0, 0, 100], - fidRanks: [0, 0, 100], - clsRanks: [0, 0, 100], + coreVitalPages: 0, + cls: null, + tbt: 0, + lcpRanks: [100, 0, 0], + fidRanks: [100, 0, 0], + clsRanks: [100, 0, 0], }); }); }); @@ -52,20 +50,21 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expectSnapshot(response.body).toMatchInline(` Object { - "cls": "0.00", + "cls": "0.000", "clsRanks": Array [ 100, 0, 0, ], - "fcp": 1072, + "coreVitalPages": 6, + "fcp": 817.5, "fid": 1352.13, "fidRanks": Array [ 0, 0, 100, ], - "lcp": 1270.5, + "lcp": 1019, "lcpRanks": Array [ 100, 0, From f88c66446119971f759315dbb9c29be9d578ca96 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:35:53 -0400 Subject: [PATCH 09/73] [Fleet]: add log statement when deleting transform (#81927) [Fleet]: add log statement when deleting transform --- .../server/services/epm/elasticsearch/transform/install.ts | 5 +++++ .../server/services/epm/elasticsearch/transform/remove.ts | 6 ++++-- .../services/epm/elasticsearch/transform/transform.test.ts | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts index 89811783a7f79..1002eedc48740 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts @@ -17,6 +17,7 @@ import { CallESAsCurrentUser } from '../../../../types'; import { getInstallation } from '../../packages'; import { deleteTransforms, deleteTransformRefs } from './remove'; import { getAsset } from './common'; +import { appContextService } from '../../../app_context'; interface TransformInstallation { installationName: string; @@ -29,6 +30,7 @@ export const installTransform = async ( callCluster: CallESAsCurrentUser, savedObjectsClient: SavedObjectsClientContract ) => { + const logger = appContextService.getLogger(); const installation = await getInstallation({ savedObjectsClient, pkgName: installablePackage.name, @@ -38,6 +40,9 @@ export const installTransform = async ( previousInstalledTransformEsAssets = installation.installed_es.filter( ({ type, id }) => type === ElasticsearchAssetType.transform ); + logger.info( + `Found previous transform references:\n ${JSON.stringify(previousInstalledTransformEsAssets)}` + ); } // delete all previous transform diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts index 5b5583a121e5d..de7aeb816f76e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts @@ -24,6 +24,8 @@ export const deleteTransforms = async ( callCluster: CallESAsCurrentUser, transformIds: string[] ) => { + const logger = appContextService.getLogger(); + logger.info(`Deleting currently installed transform ids ${transformIds}`); await Promise.all( transformIds.map(async (transformId) => { // get the index the transform @@ -47,7 +49,7 @@ export const deleteTransforms = async ( path: `/_transform/${transformId}`, ignore: [404], }); - + logger.info(`Deleted: ${transformId}`); if (transformResponse?.transforms) { // expect this to be 1 for (const transform of transformResponse.transforms) { @@ -58,7 +60,7 @@ export const deleteTransforms = async ( }); } } else { - appContextService.getLogger().warn(`cannot find transform for ${transformId}`); + logger.warn(`cannot find transform for ${transformId}`); } }) ); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts index 2bf0ad12856f8..7ca2a32cf770c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createAppContextStartContractMock } from '../../../../mocks'; + jest.mock('../../packages/get', () => { return { getInstallation: jest.fn(), getInstallationObject: jest.fn() }; }); @@ -21,11 +23,13 @@ import { getInstallation, getInstallationObject } from '../../packages'; import { getAsset } from './common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { appContextService } from '../../../app_context'; describe('test transform install', () => { let legacyScopedClusterClient: jest.Mocked; let savedObjectsClient: jest.Mocked; beforeEach(() => { + appContextService.start(createAppContextStartContractMock()); legacyScopedClusterClient = { callAsInternalUser: jest.fn(), callAsCurrentUser: jest.fn(), From 2daa511b9f05a7537707e928aeefa8369d773f68 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 28 Oct 2020 16:23:36 -0400 Subject: [PATCH 10/73] [Fleet] Test creating|copying a policy create a correct POLICY_CHANGE action (#81661) --- .../agent_policy_with_agents_setup.ts | 137 ++++++++++++++++++ .../apis/agent_policy/index.js | 1 + 2 files changed, 138 insertions(+) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts new file mode 100644 index 0000000000000..d99e6a69c5124 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { setupIngest, getSupertestWithoutAuth } from '../fleet/agents/services'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); + const kibanaServer = getService('kibanaServer'); + + async function getEnrollmentKeyForPolicyId(policyId: string) { + const listRes = await supertest.get(`/api/fleet/enrollment-api-keys`).expect(200); + + const key = listRes.body.list.find( + (item: { policy_id: string; id: string }) => item.policy_id === policyId + ); + + expect(key).not.empty(); + + const res = await supertest.get(`/api/fleet/enrollment-api-keys/${key.id}`).expect(200); + + return res.body.item; + } + + // Enroll an agent to get the actions for an agent as encrypted saved object are not expose otherwise + async function getAgentActionsForEnrollmentKey(enrollmentAPIToken: string) { + const kibanaVersionAccessor = kibanaServer.version; + const kibanaVersion = await kibanaVersionAccessor.get(); + + const { body: enrollmentResponse } = await supertestWithoutAuth + .post(`/api/ingest_manager/fleet/agents/enroll`) + .set('kbn-xsrf', 'xxx') + .set('Authorization', `ApiKey ${enrollmentAPIToken}`) + .send({ + type: 'PERMANENT', + metadata: { + local: { + elastic: { agent: { version: kibanaVersion } }, + }, + user_provided: {}, + }, + }) + .expect(200); + + const agentAccessAPIKey = enrollmentResponse.item.access_api_key; + + // Agent checkin + const { body: checkinApiResponse } = await supertestWithoutAuth + .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/checkin`) + .set('kbn-xsrf', 'xx') + .set('Authorization', `ApiKey ${agentAccessAPIKey}`) + .send({ + events: [], + }) + .expect(200); + + expect(checkinApiResponse.actions).length(1); + + return checkinApiResponse.actions[0]; + } + + // Test all the side effect that should occurs when we create|update an agent policy + describe('ingest_manager_agent_policies_with_agents_setup', () => { + skipIfNoDockerRegistry(providerContext); + + before(async () => { + await esArchiver.loadIfNeeded('fleet/agents'); + }); + after(async () => { + await esArchiver.unload('fleet/agents'); + }); + + setupIngest(providerContext); + + describe('POST /api/fleet/agent_policies', () => { + it('should create an enrollment key and an agent action `POLICY_CHANGE` for the policy', async () => { + const name = `test-${Date.now()}`; + + const res = await supertest + .post(`/api/fleet/agent_policies?sys_monitoring=true`) + .set('kbn-xsrf', 'xxxx') + .send({ + name, + namespace: 'default', + }) + .expect(200); + + const policyId = res.body.item.id; + const enrollmentKey = await getEnrollmentKeyForPolicyId(policyId); + expect(enrollmentKey).not.empty(); + + const action = await getAgentActionsForEnrollmentKey(enrollmentKey.api_key); + + expect(action.type).to.be('POLICY_CHANGE'); + const agentPolicy = action.data.policy; + expect(agentPolicy.id).to.be(policyId); + // should have system inputs + expect(agentPolicy.inputs).length(2); + // should have default output + expect(agentPolicy.outputs.default).not.empty(); + }); + }); + + describe('POST /api/fleet/agent_policies/copy', () => { + const TEST_POLICY_ID = `policy1`; + + it('should create an enrollment key and an agent action `POLICY_CHANGE` for the policy', async () => { + const name = `test-${Date.now()}`; + + const res = await supertest + .post(`/api/fleet/agent_policies/${TEST_POLICY_ID}/copy`) + .set('kbn-xsrf', 'xxxx') + .send({ + name, + description: 'Test', + }) + .expect(200); + + const policyId = res.body.item.id; + const enrollmentKey = await getEnrollmentKeyForPolicyId(policyId); + expect(enrollmentKey).not.empty(); + + const action = await getAgentActionsForEnrollmentKey(enrollmentKey.api_key); + expect(action.type).to.be('POLICY_CHANGE'); + expect(action.data.policy.id).to.be(policyId); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js index a513e7991fa74..da608b7057c0a 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js @@ -6,6 +6,7 @@ export default function loadTests({ loadTestFile }) { describe('Ingest Manager Endpoints', () => { + loadTestFile(require.resolve('./agent_policy_with_agents_setup')); loadTestFile(require.resolve('./agent_policy')); }); } From 526de26f034e43521c911e12899ce523a73be607 Mon Sep 17 00:00:00 2001 From: IgorG <56408662+IgorGuz2000@users.noreply.github.com> Date: Wed, 28 Oct 2020 14:02:29 -0700 Subject: [PATCH 11/73] [Feature:Resolver] enable_APM-ci branch fixes (#81658) * Added Test for event.library * renamed data directry and gzip data file * rename expectedData file * Changes per Charlie request * Changes for the enable_APM-ci branch * Update resolver.ts * Added comment per Charlie request * Update resolver.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/endpoint/resolver.ts | 12 +++++++++++- .../page_objects/hosts_page.ts | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index 1af9ec88df852..b45c082423628 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts @@ -262,11 +262,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load('endpoint/resolver_tree/library_events', { useCreate: true }); await queryBar.setQuery(''); await queryBar.submitQuery(); - const expectedLibraryData = ['329 network', '1 library', '1 library']; + const expectedLibraryData = [ + '1 authentication', + '1 session', + '329 network', + '1 library', + '1 library', + ]; await pageObjects.hosts.navigateToEventsPanel(); await pageObjects.hosts.executeQueryAndOpenResolver( 'event.dataset : endpoint.events.library' ); + // This lines will move the resolver view for clear visibility of the related events. + for (let i = 0; i < 7; i++) { + await (await testSubjects.find('resolver:graph-controls:west-button')).click(); + } await pageObjects.hosts.runNodeEvents(expectedLibraryData); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts index 3301217e41a90..988fadbbdfe33 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts @@ -127,6 +127,7 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro expect(EventName).to.equal(linkText); expect(EventName).to.equal(expectedData[i]); } + await testSubjects.click('full-screen'); }, /** * Navigate to Events Panel @@ -146,7 +147,6 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro await queryBar.submitQuery(); await testSubjects.click('full-screen'); await testSubjects.click('investigate-in-resolver-button'); - await testSubjects.click('full-screen'); }, }; } From 271a799ef8e4b02092f387bf3622bacabf9b51bd Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 28 Oct 2020 14:08:56 -0700 Subject: [PATCH 12/73] [jenkins] disable CI metrics for temporary feature branches (#81938) Co-authored-by: spalger --- vars/getCheckoutInfo.groovy | 5 +++++ vars/githubPr.groovy | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/vars/getCheckoutInfo.groovy b/vars/getCheckoutInfo.groovy index 32a7b054bfd15..f9d797f8127c7 100644 --- a/vars/getCheckoutInfo.groovy +++ b/vars/getCheckoutInfo.groovy @@ -2,6 +2,7 @@ def call(branchOverride) { def repoInfo = [ branch: branchOverride ?: env.ghprbSourceBranch, targetBranch: env.ghprbTargetBranch, + targetsTrackedBranch: true ] if (repoInfo.branch == null) { @@ -35,6 +36,10 @@ def call(branchOverride) { label: "determining merge point with '${repoInfo.targetBranch}' at origin", returnStdout: true ).trim() + + def pkgJson = readFile("package.json") + def releaseBranch = toJSON(pkgJson).branch + repoInfo.targetsTrackedBranch = releaseBranch == repoInfo.targetBranch } print "repoInfo: ${repoInfo}" diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index fd5412c905683..546a6785ac2f4 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -149,7 +149,7 @@ def getTestFailuresMessage() { def getBuildStatusIncludingMetrics() { def status = buildUtils.getBuildStatus() - if (status == 'SUCCESS' && !ciStats.getMetricsSuccess()) { + if (status == 'SUCCESS' && shouldCheckCiMetricSuccess() && !ciStats.getMetricsSuccess()) { return 'FAILURE' } @@ -297,3 +297,12 @@ def getFailedSteps() { step.displayName != 'Check out from version control' } } + +def shouldCheckCiMetricSuccess() { + // disable ciMetrics success check when a PR is targetting a non-tracked branch + if (buildState.has('checkoutInfo') && !buildState.get('checkoutInfo').targetsTrackedBranch) { + return false + } + + return true +} From 91a84d5d0fc40ce26bee2804c78d92e6f9ed24a1 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 28 Oct 2020 17:20:55 -0500 Subject: [PATCH 13/73] Fix regression in our ml usage collection (#81945) A regression was introduced in #74965 that caused an error to be thrown while collecting telemetry on ML jobs. Because such exceptions are caught and we degrade to zeroing out those counts, this one was not caught until manual testing of telemetry. --- .../server/usage/detections/detections_helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index 5cf17af2fa9c0..6387839db3bfe 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -172,7 +172,7 @@ export const getMlJobsUsage = async (ml: MlPluginSetup | undefined): Promise Date: Wed, 28 Oct 2020 17:58:05 -0500 Subject: [PATCH 14/73] [deb/rpm] kibana.service cleanup (#75219) --- .../systemd/etc/systemd/system/kibana.service | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service index e66e0e7c8dfb5..df33b82f1f967 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service @@ -1,21 +1,32 @@ [Unit] Description=Kibana +Documentation=https://www.elastic.co +Wants=network-online.target +After=network-online.target [Service] Type=simple User=kibana Group=kibana -# Load env vars from /etc/default/ and /etc/sysconfig/ if they exist. -# Prefixing the path with '-' makes it try to load, but if the file doesn't -# exist, it continues onward. + +Environment=KBN_HOME=/usr/share/kibana +Environment=KBN_PATH_CONF=/etc/kibana + EnvironmentFile=-/etc/default/kibana EnvironmentFile=-/etc/sysconfig/kibana + ExecStart=/usr/share/kibana/bin/kibana + Restart=on-failure RestartSec=3 + StartLimitBurst=3 StartLimitInterval=60 -WorkingDirectory=/ + +WorkingDirectory=/usr/share/kibana + +StandardOutput=journal +StandardError=inherit [Install] WantedBy=multi-user.target From bd64a0bb95ba109e0bb2b26157e63aecb5fa79de Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 28 Oct 2020 16:30:11 -0700 Subject: [PATCH 15/73] Adding a comment about the NeededFor: label (#81939) --- .github/ISSUE_TEMPLATE/v8_breaking_change.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md index c91b937586a09..42783808e32ed 100644 --- a/.github/ISSUE_TEMPLATE/v8_breaking_change.md +++ b/.github/ISSUE_TEMPLATE/v8_breaking_change.md @@ -7,6 +7,16 @@ assignees: '' --- + + ## Change description **Which release will ship the breaking change?** From 3af1099ba830bcccc04094c5e251f2b2d6f3309b Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 28 Oct 2020 16:38:59 -0700 Subject: [PATCH 16/73] [browserlist] Excludes browsers not supporting es6-class (#81431) * Updates browserlist to exclude those not supporting es6-class Signed-off-by: Tyler Smalley --- .browserslistrc | 2 ++ .../__snapshots__/basic_optimization.test.ts.snap | 2 +- .../maps/public/classes/tooltips/join_tooltip_property.ts | 6 ++++-- yarn.lock | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.browserslistrc b/.browserslistrc index 04395b913c9c5..36298c0f8cb93 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -4,6 +4,8 @@ last 2 Chrome versions last 2 Safari versions > 0.25% not ie 11 +not op_mini all +not samsung 4 [dev] last 1 chrome versions diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index cb5bb1e8fc529..ddb19c8cdc3d7 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -108,4 +108,4 @@ exports[`prepares assets for distribution: baz bundle 1`] = ` exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; -exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { const esFilters = []; if (this._tooltipProperty.isFilterable()) { - esFilters.push(...(await this._tooltipProperty.getESFilters())); + const filters = await this._tooltipProperty.getESFilters(); + esFilters.push(...filters); } for (let i = 0; i < this._leftInnerJoins.length; i++) { @@ -51,7 +52,8 @@ export class JoinTooltipProperty implements ITooltipProperty { this._tooltipProperty.getRawValue() ); if (esTooltipProperty) { - esFilters.push(...(await esTooltipProperty.getESFilters())); + const filters = await esTooltipProperty.getESFilters(); + esFilters.push(...filters); } } catch (e) { // eslint-disable-next-line no-console diff --git a/yarn.lock b/yarn.lock index 8de8e0a8c0eb2..b2216537bbd7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8153,9 +8153,9 @@ camelize@^1.0.0: integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001097: - version "1.0.30001114" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001114.tgz#2e88119afb332ead5eaa330e332e951b1c4bfea9" - integrity sha512-ml/zTsfNBM+T1+mjglWRPgVsu2L76GAaADKX5f4t0pbhttEp0WMawJsHDYlFkVZkoA+89uvBRrVrEE4oqenzXQ== + version "1.0.30001150" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz" + integrity sha512-kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ== capture-exit@^2.0.0: version "2.0.0" From 06c99bbf7e102683414809c99cffa636af54eaaf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 28 Oct 2020 18:37:09 -0600 Subject: [PATCH 17/73] [Maps] consolidate map saved object loading into MapApp component (#81914) * [Maps] consolidate map saved object loading into MapApp component * tslint * more tslint cleanup * tslint * review feedback --- .../public/lazy_load_bundle/lazy/index.ts | 2 +- .../{maps_router.tsx => render_app.tsx} | 43 +++----- .../routing/routes/list/maps_list_view.tsx | 2 +- .../get_breadcrumbs.test.tsx | 2 +- .../{maps_app => map_app}/get_breadcrumbs.tsx | 2 +- .../routes/{maps_app => map_app}/index.ts | 6 +- .../maps_app_view.tsx => map_app/map_app.tsx} | 99 +++++++++++++------ .../{maps_app => map_app}/top_nav_config.tsx | 6 +- .../routes/maps_app/load_map_and_render.tsx | 86 ---------------- .../public/routing/state_syncing/app_sync.ts | 2 +- .../routing/state_syncing/global_sync.ts | 2 +- 11 files changed, 92 insertions(+), 160 deletions(-) rename x-pack/plugins/maps/public/routing/{maps_router.tsx => render_app.tsx} (81%) rename x-pack/plugins/maps/public/routing/routes/{maps_app => map_app}/get_breadcrumbs.test.tsx (96%) rename x-pack/plugins/maps/public/routing/routes/{maps_app => map_app}/get_breadcrumbs.tsx (96%) rename x-pack/plugins/maps/public/routing/routes/{maps_app => map_app}/index.ts (94%) rename x-pack/plugins/maps/public/routing/routes/{maps_app/maps_app_view.tsx => map_app/map_app.tsx} (85%) rename x-pack/plugins/maps/public/routing/routes/{maps_app => map_app}/top_nav_config.tsx (98%) delete mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 782d645dc230a..067f213603fe3 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -15,7 +15,7 @@ export * from '../../actions'; export * from '../../selectors/map_selectors'; export * from '../../routing/bootstrap/get_initial_layers'; export * from '../../embeddable/merge_input_with_saved_map'; -export * from '../../routing/maps_router'; +export { renderApp } from '../../routing/render_app'; export * from '../../classes/layers/solution_layers/security'; export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry'; export { registerSource } from '../../classes/sources/source_registry'; diff --git a/x-pack/plugins/maps/public/routing/maps_router.tsx b/x-pack/plugins/maps/public/routing/render_app.tsx similarity index 81% rename from x-pack/plugins/maps/public/routing/maps_router.tsx rename to x-pack/plugins/maps/public/routing/render_app.tsx index d7e6e6e079953..65cccc53f5047 100644 --- a/x-pack/plugins/maps/public/routing/maps_router.tsx +++ b/x-pack/plugins/maps/public/routing/render_app.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom'; +import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { Provider } from 'react-redux'; import { AppMountParameters } from 'kibana/public'; @@ -24,13 +24,12 @@ import { } from '../../../../../src/plugins/kibana_utils/public'; import { getStore } from './store_operations'; import { LoadListAndRender } from './routes/list/load_list_and_render'; -import { LoadMapAndRender } from './routes/maps_app/load_map_and_render'; +import { MapApp } from './routes/map_app'; export let goToSpecifiedPath: (path: string) => void; export let kbnUrlStateStorage: IKbnUrlStateStorage; export async function renderApp({ - appBasePath, element, history, onAppLeave, @@ -43,29 +42,6 @@ export async function renderApp({ ...withNotifyOnErrors(getToasts()), }); - render( - , - element - ); - - return () => { - unmountComponentAtNode(element); - }; -} - -interface Props { - history: AppMountParameters['history'] | RouteComponentProps['history']; - appBasePath: AppMountParameters['appBasePath']; - onAppLeave: AppMountParameters['onAppLeave']; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; -} - -const App: React.FC = ({ history, appBasePath, onAppLeave, setHeaderActionMenu }) => { const store = getStore(); const I18nContext = getCoreI18n().Context; @@ -88,7 +64,7 @@ const App: React.FC = ({ history, appBasePath, onAppLeave, setHeaderActio }); } - return ( + render( @@ -96,7 +72,7 @@ const App: React.FC = ({ history, appBasePath, onAppLeave, setHeaderActio ( - = ({ history, appBasePath, onAppLeave, setHeaderActio exact path={`/map`} render={() => ( - = ({ history, appBasePath, onAppLeave, setHeaderActio - + , + element ); -}; + + return () => { + unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx index 3b0891a4fd44d..ca92442ae93e6 100644 --- a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx @@ -30,7 +30,7 @@ import { EuiBasicTableColumn, } from '@elastic/eui/src/components/basic_table/basic_table'; import { EuiTableSortingType } from '@elastic/eui'; -import { goToSpecifiedPath } from '../../maps_router'; +import { goToSpecifiedPath } from '../../render_app'; // @ts-expect-error import { addHelpMenuToAppChrome } from '../../../help_menu_util'; import { APP_ID, MAP_PATH } from '../../../../common/constants'; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.test.tsx similarity index 96% rename from x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx rename to x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.test.tsx index e8e0e583a7c6d..3516daf526968 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx +++ b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.test.tsx @@ -7,7 +7,7 @@ import { getBreadcrumbs } from './get_breadcrumbs'; jest.mock('../../../kibana_services', () => {}); -jest.mock('../../maps_router', () => {}); +jest.mock('../../render_app', () => {}); const getHasUnsavedChanges = () => { return false; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.tsx similarity index 96% rename from x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx rename to x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.tsx index d9b60b670b93e..88dba0f83ec2f 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx +++ b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { getCoreOverlays, getNavigateToApp } from '../../../kibana_services'; -import { goToSpecifiedPath } from '../../maps_router'; +import { goToSpecifiedPath } from '../../render_app'; import { getAppTitle } from '../../../../common/i18n_getters'; export const unsavedChangesWarning = i18n.translate( diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts b/x-pack/plugins/maps/public/routing/routes/map_app/index.ts similarity index 94% rename from x-pack/plugins/maps/public/routing/routes/maps_app/index.ts rename to x-pack/plugins/maps/public/routing/routes/map_app/index.ts index 812d7fcf30981..0b9f0cfe33e44 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts +++ b/x-pack/plugins/maps/public/routing/routes/map_app/index.ts @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { ThunkDispatch } from 'redux-thunk'; import { AnyAction } from 'redux'; import { Filter, Query, TimeRange } from 'src/plugins/data/public'; -import { MapsAppView } from './maps_app_view'; +import { MapApp } from './map_app'; import { getFlyoutDisplay, getIsFullScreen } from '../../../selectors/ui_selectors'; import { getFilters, @@ -99,5 +99,5 @@ function mapDispatchToProps(dispatch: ThunkDispatch { +export class MapApp extends React.Component { _globalSyncUnsubscribe: (() => void) | null = null; _globalSyncChangeMonitorSubscription: Subscription | null = null; _appSyncUnsubscribe: (() => void) | null = null; @@ -134,8 +139,6 @@ export class MapsAppView extends React.Component { this._initMap(); - this._setBreadcrumbs(); - this.props.onAppLeave((actions) => { if (this._hasUnsavedChanges()) { return actions.confirm(unsavedChangesWarning, unsavedChangesTitle); @@ -165,7 +168,11 @@ export class MapsAppView extends React.Component { } _hasUnsavedChanges = () => { - const savedLayerList = this.props.savedMap.getLayerList(); + if (!this.state.savedMap) { + return false; + } + + const savedLayerList = this.state.savedMap.getLayerList(); return !savedLayerList ? !_.isEqual(this.props.layerListConfigOnly, this.state.initialLayerListConfig) : // savedMap stores layerList as a JSON string using JSON.stringify. @@ -176,9 +183,9 @@ export class MapsAppView extends React.Component { !_.isEqual(JSON.parse(JSON.stringify(this.props.layerListConfigOnly)), savedLayerList); }; - _setBreadcrumbs = () => { + _setBreadcrumbs = (title: string) => { const breadcrumbs = getBreadcrumbs({ - title: this.props.savedMap.title, + title, getHasUnsavedChanges: this._hasUnsavedChanges, originatingApp: this.state.originatingApp, getAppNameFromId: this.props.stateTransfer.getAppNameFromId, @@ -255,13 +262,12 @@ export class MapsAppView extends React.Component { updateGlobalState(updatedGlobalState, !this.state.initialized); }; - _initMapAndLayerSettings() { + _initMapAndLayerSettings(savedMap: ISavedGisMap) { const globalState: MapsGlobalState = getGlobalState(); - const mapStateJSON = this.props.savedMap.mapStateJSON; let savedObjectFilters = []; - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); + if (savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); if (mapState.filters) { savedObjectFilters = mapState.filters; } @@ -269,7 +275,7 @@ export class MapsAppView extends React.Component { const appFilters = this._appStateManager.getFilters() || []; const query = getInitialQuery({ - mapStateJSON, + mapStateJSON: savedMap.mapStateJSON, appState: this._appStateManager.getAppState(), }); if (query) { @@ -280,22 +286,19 @@ export class MapsAppView extends React.Component { filters: [..._.get(globalState, 'filters', []), ...appFilters, ...savedObjectFilters], query, time: getInitialTimeFilters({ - mapStateJSON, + mapStateJSON: savedMap.mapStateJSON, globalState, }), }); this._onRefreshConfigChange( getInitialRefreshConfig({ - mapStateJSON, + mapStateJSON: savedMap.mapStateJSON, globalState, }) ); - const layerList = getInitialLayers( - this.props.savedMap.layerListJSON, - getInitialLayersFromUrlParam() - ); + const layerList = getInitialLayers(savedMap.layerListJSON, getInitialLayersFromUrlParam()); this.props.replaceLayerList(layerList); this.setState({ initialLayerListConfig: copyPersistentState(layerList), @@ -345,13 +348,43 @@ export class MapsAppView extends React.Component { }); }; - _initMap() { - this._initMapAndLayerSettings(); + async _loadSavedMap(): Promise { + let savedMap: ISavedGisMap | null = null; + try { + savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); + } catch (err) { + if (this._isMounted) { + getToasts().addWarning({ + title: i18n.translate('xpack.maps.loadMap.errorAttemptingToLoadSavedMap', { + defaultMessage: `Unable to load map`, + }), + text: `${err.message}`, + }); + goToSpecifiedPath('/'); + } + } + + return savedMap; + } + + async _initMap() { + const savedMap = await this._loadSavedMap(); + if (!this._isMounted || !savedMap) { + return; + } + + this._setBreadcrumbs(savedMap.title); + getCoreChrome().docTitle.change(savedMap.title); + if (this.props.savedMapId) { + getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id!); + } + + this._initMapAndLayerSettings(savedMap); this.props.clearUi(); - if (this.props.savedMap.mapStateJSON) { - const mapState = JSON.parse(this.props.savedMap.mapStateJSON); + if (savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); this.props.setGotoWithCenter({ lat: mapState.center.lat, lon: mapState.center.lon, @@ -362,22 +395,22 @@ export class MapsAppView extends React.Component { } } - if (this.props.savedMap.uiStateJSON) { - const uiState = JSON.parse(this.props.savedMap.uiStateJSON); + if (savedMap.uiStateJSON) { + const uiState = JSON.parse(savedMap.uiStateJSON); this.props.setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)); this.props.setOpenTOCDetails(_.get(uiState, 'openTOCDetails', [])); } - this.setState({ initialized: true }); + this.setState({ initialized: true, savedMap }); } _renderTopNav() { - if (this.props.isFullScreen) { + if (this.props.isFullScreen || !this.state.savedMap) { return null; } const topNavConfig = getTopNavConfig({ - savedMap: this.props.savedMap, + savedMap: this.state.savedMap, isOpenSettingsDisabled: this.props.isOpenSettingsDisabled, isSaveDisabled: this.props.isSaveDisabled, enableFullScreen: this.props.enableFullScreen, @@ -452,18 +485,22 @@ export class MapsAppView extends React.Component { }; render() { - return this.state.initialized ? ( + if (!this.state.initialized || !this.state.savedMap) { + return null; + } + + return (
{this._renderTopNav()}

{`screenTitle placeholder`}

- ) : null; + ); } } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx b/x-pack/plugins/maps/public/routing/routes/map_app/top_nav_config.tsx similarity index 98% rename from x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx rename to x-pack/plugins/maps/public/routing/routes/map_app/top_nav_config.tsx index 917abebfb6b25..c60f44093541f 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routing/routes/map_app/top_nav_config.tsx @@ -21,7 +21,7 @@ import { showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; -import { goToSpecifiedPath } from '../../maps_router'; +import { goToSpecifiedPath } from '../../render_app'; import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; import { EmbeddableStateTransfer } from '../../../../../../../src/plugins/embeddable/public'; @@ -43,7 +43,7 @@ export function getTopNavConfig({ enableFullScreen: () => void; openMapSettings: () => void; inspectorAdapters: Adapters; - setBreadcrumbs: () => void; + setBreadcrumbs: (title: string) => void; stateTransfer?: EmbeddableStateTransfer; originatingApp?: string; cutOriginatingAppConnection: () => void; @@ -104,7 +104,7 @@ export function getTopNavConfig({ }); getCoreChrome().docTitle.change(savedMap.title); - setBreadcrumbs(); + setBreadcrumbs(savedMap.title); goToSpecifiedPath(`/map/${savedObjectId}${window.location.hash}`); const newlyCreated = newCopyOnSave || isNewMap; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx deleted file mode 100644 index b980756daad20..0000000000000 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx +++ /dev/null @@ -1,86 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { Redirect } from 'react-router-dom'; -import { AppMountParameters } from 'kibana/public'; -import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; -import { getCoreChrome, getToasts } from '../../../kibana_services'; -import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; -import { MapsAppView } from '.'; -import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; - -interface Props { - savedMapId?: string; - onAppLeave: AppMountParameters['onAppLeave']; - stateTransfer: EmbeddableStateTransfer; - originatingApp?: string; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; -} - -interface State { - savedMap?: ISavedGisMap; - failedToLoad: boolean; -} - -export const LoadMapAndRender = class extends React.Component { - _isMounted: boolean = false; - state: State = { - savedMap: undefined, - failedToLoad: false, - }; - - componentDidMount() { - this._isMounted = true; - this._loadSavedMap(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - async _loadSavedMap() { - try { - const savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); - if (this._isMounted) { - getCoreChrome().docTitle.change(savedMap.title); - if (this.props.savedMapId) { - getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); - } - this.setState({ savedMap }); - } - } catch (err) { - if (this._isMounted) { - this.setState({ failedToLoad: true }); - getToasts().addWarning({ - title: i18n.translate('xpack.maps.loadMap.errorAttemptingToLoadSavedMap', { - defaultMessage: `Unable to load map`, - }), - text: `${err.message}`, - }); - } - } - } - - render() { - const { savedMap, failedToLoad } = this.state; - - if (failedToLoad) { - return ; - } - - return savedMap ? ( - - ) : null; - } -}; diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts index b346822913bec..498442040681c 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts @@ -8,7 +8,7 @@ import { map } from 'rxjs/operators'; import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public'; import { syncState, BaseStateContainer } from '../../../../../../src/plugins/kibana_utils/public'; import { getData } from '../../kibana_services'; -import { kbnUrlStateStorage } from '../maps_router'; +import { kbnUrlStateStorage } from '../render_app'; import { AppStateManager } from './app_state_manager'; export function startAppStateSyncing(appStateManager: AppStateManager) { diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts index 1e779831c5e0c..3f370d9aa99b2 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts +++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts @@ -6,7 +6,7 @@ import { TimeRange, RefreshInterval, Filter } from 'src/plugins/data/public'; import { syncQueryStateWithUrl } from '../../../../../../src/plugins/data/public'; import { getData } from '../../kibana_services'; -import { kbnUrlStateStorage } from '../maps_router'; +import { kbnUrlStateStorage } from '../render_app'; export interface MapsGlobalState { time?: TimeRange; From 213469d5cd2192f91ab2b1f87c617f03323c6b85 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 29 Oct 2020 10:56:17 +0100 Subject: [PATCH 18/73] Properly handle session index initialization failures. (#81894) --- .../server/session_management/session_index.test.ts | 11 +++++++++++ .../server/session_management/session_index.ts | 11 ++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts index f4ff5a8bddb74..cba63412bf502 100644 --- a/x-pack/plugins/security/server/session_management/session_index.test.ts +++ b/x-pack/plugins/security/server/session_management/session_index.test.ts @@ -155,6 +155,17 @@ describe('Session index', () => { await sessionIndex.initialize(); }); + + it('works properly after failure', async () => { + const unexpectedError = new Error('Uh! Oh!'); + mockClusterClient.callAsInternalUser.mockImplementationOnce(() => + Promise.reject(unexpectedError) + ); + mockClusterClient.callAsInternalUser.mockImplementationOnce(() => Promise.resolve(true)); + + await expect(sessionIndex.initialize()).rejects.toBe(unexpectedError); + await expect(sessionIndex.initialize()).resolves.toBe(undefined); + }); }); describe('cleanUp', () => { diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 191e71f14d66d..ee503acc0d3a4 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -276,7 +276,7 @@ export class SessionIndex { } const sessionIndexTemplateName = `${this.options.kibanaIndexName}_security_session_index_template_${SESSION_INDEX_TEMPLATE_VERSION}`; - return (this.indexInitialization = new Promise(async (resolve) => { + return (this.indexInitialization = new Promise(async (resolve, reject) => { // Check if required index template exists. let indexTemplateExists = false; try { @@ -288,7 +288,7 @@ export class SessionIndex { this.options.logger.error( `Failed to check if session index template exists: ${err.message}` ); - throw err; + return reject(err); } // Create index template if it doesn't exist. @@ -303,7 +303,7 @@ export class SessionIndex { this.options.logger.debug('Successfully created session index template.'); } catch (err) { this.options.logger.error(`Failed to create session index template: ${err.message}`); - throw err; + return reject(err); } } @@ -316,7 +316,7 @@ export class SessionIndex { }); } catch (err) { this.options.logger.error(`Failed to check if session index exists: ${err.message}`); - throw err; + return reject(err); } // Create index if it doesn't exist. @@ -334,13 +334,14 @@ export class SessionIndex { this.options.logger.debug('Session index already exists.'); } else { this.options.logger.error(`Failed to create session index: ${err.message}`); - throw err; + return reject(err); } } } // Notify any consumers that are awaiting on this promise and immediately reset it. resolve(); + }).finally(() => { this.indexInitialization = undefined; })); } From 66d79ea2bf5c743298e2e71c1fb44caec4920d4a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 29 Oct 2020 11:24:10 +0000 Subject: [PATCH 19/73] Reactively disable Task Manager lifecycle when core services become unavailable (#81779) Plugs the Task Manager polling lifecycle into the Kibana Services Status streams in order to ensure we reactively start and stop polling whenever the Elasticsearch or SavedObjects service switch between `available` and `unavailable`. This will prevent Task Manager from polling whenever these services switch to an `unavailable` state. --- .../task_manager/server/monitoring/index.ts | 10 +- .../monitoring/monitoring_stats_stream.ts | 2 + .../monitoring/workload_statistics.test.ts | 48 ++++++++- .../server/monitoring/workload_statistics.ts | 8 +- .../task_manager/server/plugin.test.ts | 100 +++++++++++++++++- x-pack/plugins/task_manager/server/plugin.ts | 48 ++++++--- .../server/polling_lifecycle.mock.ts | 2 - .../server/polling_lifecycle.test.ts | 57 +++++++++- .../task_manager/server/polling_lifecycle.ts | 100 ++++++++++-------- 9 files changed, 301 insertions(+), 74 deletions(-) diff --git a/x-pack/plugins/task_manager/server/monitoring/index.ts b/x-pack/plugins/task_manager/server/monitoring/index.ts index 8e71ce2519a7c..0a4c8c56a5a79 100644 --- a/x-pack/plugins/task_manager/server/monitoring/index.ts +++ b/x-pack/plugins/task_manager/server/monitoring/index.ts @@ -28,12 +28,20 @@ export { export function createMonitoringStats( taskPollingLifecycle: TaskPollingLifecycle, taskStore: TaskStore, + elasticsearchAndSOAvailability$: Observable, config: TaskManagerConfig, managedConfig: ManagedConfiguration, logger: Logger ): Observable { return createMonitoringStatsStream( - createAggregators(taskPollingLifecycle, taskStore, config, managedConfig, logger), + createAggregators( + taskPollingLifecycle, + taskStore, + elasticsearchAndSOAvailability$, + config, + managedConfig, + logger + ), config ); } diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts index 374660a257c59..524afb8d78e21 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -63,6 +63,7 @@ export interface RawMonitoringStats { export function createAggregators( taskPollingLifecycle: TaskPollingLifecycle, taskStore: TaskStore, + elasticsearchAndSOAvailability$: Observable, config: TaskManagerConfig, managedConfig: ManagedConfiguration, logger: Logger @@ -72,6 +73,7 @@ export function createAggregators( createTaskRunAggregator(taskPollingLifecycle, config.monitored_stats_running_average_window), createWorkloadAggregator( taskStore, + elasticsearchAndSOAvailability$, config.monitored_aggregated_stats_refresh_rate, config.poll_interval, logger diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index d9af3307e75cb..cb6e48530b027 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -17,6 +17,8 @@ import { ESSearchResponse } from '../../../apm/typings/elasticsearch'; import { AggregationResultOf } from '../../../apm/typings/elasticsearch/aggregations'; import { times } from 'lodash'; import { taskStoreMock } from '../task_store.mock'; +import { of, Subject } from 'rxjs'; +import { sleep } from '../test_utils'; type MockESResult = ESSearchResponse< ConcreteTaskInstance, @@ -75,6 +77,7 @@ describe('Workload Statistics Aggregator', () => { const workloadAggregator = createWorkloadAggregator( taskStore, + of(true), 10, 3000, loggingSystemMock.create().get() @@ -231,6 +234,7 @@ describe('Workload Statistics Aggregator', () => { const workloadAggregator = createWorkloadAggregator( taskStore, + of(true), 10, 3000, loggingSystemMock.create().get() @@ -252,12 +256,51 @@ describe('Workload Statistics Aggregator', () => { }); }); + test('skips summary of the workload when services are unavailable', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); + + const availability$ = new Subject(); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + availability$, + 10, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise(async (resolve) => { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + expect(result.value).toMatchObject({ + count: 4, + task_types: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 1, status: { idle: 1 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + }); + resolve(); + }); + + availability$.next(false); + + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + availability$.next(true); + }); + }); + test('returns a count of the overdue workload', async () => { const taskStore = taskStoreMock.create({}); taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); const workloadAggregator = createWorkloadAggregator( taskStore, + of(true), 10, 3000, loggingSystemMock.create().get() @@ -280,6 +323,7 @@ describe('Workload Statistics Aggregator', () => { const workloadAggregator = createWorkloadAggregator( taskStore, + of(true), 10, 3000, loggingSystemMock.create().get() @@ -307,6 +351,7 @@ describe('Workload Statistics Aggregator', () => { const workloadAggregator = createWorkloadAggregator( taskStore, + of(true), 60 * 1000, 3000, loggingSystemMock.create().get() @@ -344,6 +389,7 @@ describe('Workload Statistics Aggregator', () => { const workloadAggregator = createWorkloadAggregator( taskStore, + of(true), 15 * 60 * 1000, 3000, loggingSystemMock.create().get() @@ -392,7 +438,7 @@ describe('Workload Statistics Aggregator', () => { }) ); const logger = loggingSystemMock.create().get(); - const workloadAggregator = createWorkloadAggregator(taskStore, 10, 3000, logger); + const workloadAggregator = createWorkloadAggregator(taskStore, of(true), 10, 3000, logger); return new Promise((resolve, reject) => { workloadAggregator.pipe(take(2), bufferCount(2)).subscribe((results) => { diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index fe70f24684ad9..17448ea412ae6 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { timer } from 'rxjs'; -import { mergeMap, map, catchError } from 'rxjs/operators'; +import { combineLatest, Observable, timer } from 'rxjs'; +import { mergeMap, map, filter, catchError } from 'rxjs/operators'; import { Logger } from 'src/core/server'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { keyBy, mapValues } from 'lodash'; @@ -94,6 +94,7 @@ const MAX_SHCEDULE_DENSITY_BUCKETS = 50; export function createWorkloadAggregator( taskStore: TaskStore, + elasticsearchAndSOAvailability$: Observable, refreshInterval: number, pollInterval: number, logger: Logger @@ -105,7 +106,8 @@ export function createWorkloadAggregator( MAX_SHCEDULE_DENSITY_BUCKETS ); - return timer(0, refreshInterval).pipe( + return combineLatest([timer(0, refreshInterval), elasticsearchAndSOAvailability$]).pipe( + filter(([, areElasticsearchAndSOAvailable]) => areElasticsearchAndSOAvailable), mergeMap(() => taskStore.aggregate({ aggs: { diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 8388468164a4f..9a1d83f6195ab 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TaskManagerPlugin } from './plugin'; +import { TaskManagerPlugin, getElasticsearchAndSOAvailability } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { TaskManagerConfig } from './config'; +import { Subject } from 'rxjs'; +import { bufferCount, take } from 'rxjs/operators'; +import { CoreStatus, ServiceStatusLevels } from 'src/core/server'; describe('TaskManagerPlugin', () => { describe('setup', () => { @@ -88,4 +91,99 @@ describe('TaskManagerPlugin', () => { ); }); }); + + describe('getElasticsearchAndSOAvailability', () => { + test('returns true when both services are available', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(1), bufferCount(1)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + expect(await availability).toEqual([true]); + }); + + test('returns false when both services are unavailable', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(1), bufferCount(1)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: false })); + + expect(await availability).toEqual([false]); + }); + + test('returns false when one service is unavailable but the other is available', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(1), bufferCount(1)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: false })); + + expect(await availability).toEqual([false]); + }); + + test('shift back and forth between values as status changes', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(3), bufferCount(3)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: false })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: false })); + + expect(await availability).toEqual([false, true, false]); + }); + + test(`skips values when the status hasn't changed`, async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(3), bufferCount(3)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: false })); + + // still false, so shouldn't emit a second time + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: true })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + // shouldn't emit as already true + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: false })); + + expect(await availability).toEqual([false, true, false]); + }); + }); }); + +function mockCoreStatusAvailability({ + elasticsearch, + savedObjects, +}: { + elasticsearch: boolean; + savedObjects: boolean; +}) { + return { + elasticsearch: { + level: elasticsearch ? ServiceStatusLevels.available : ServiceStatusLevels.unavailable, + summary: '', + }, + savedObjects: { + level: savedObjects ? ServiceStatusLevels.available : ServiceStatusLevels.unavailable, + summary: '', + }, + }; +} diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 0e7abb817490a..70688cd169d7e 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -3,9 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext, Plugin, CoreSetup, Logger, CoreStart } from 'src/core/server'; -import { combineLatest, Subject } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { combineLatest, Observable, Subject } from 'rxjs'; +import { first, map, distinctUntilChanged } from 'rxjs/operators'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + Logger, + CoreStart, + ServiceStatusLevels, + CoreStatus, +} from '../../../../src/core/server'; import { TaskDefinition } from './task'; import { TaskPollingLifecycle } from './polling_lifecycle'; import { TaskManagerConfig } from './config'; @@ -37,6 +45,7 @@ export class TaskManagerPlugin private logger: Logger; private definitions: TaskTypeDictionary; private middleware: Middleware = createInitialMiddleware(); + private elasticsearchAndSOAvailability$?: Observable; private monitoringStats$ = new Subject(); constructor(private readonly initContext: PluginInitializerContext) { @@ -51,6 +60,8 @@ export class TaskManagerPlugin .pipe(first()) .toPromise(); + this.elasticsearchAndSOAvailability$ = getElasticsearchAndSOAvailability(core.status.core$); + setupSavedObjects(core.savedObjects, this.config); this.taskManagerId = this.initContext.env.instanceUuid; @@ -115,19 +126,20 @@ export class TaskManagerPlugin startingPollInterval: this.config!.poll_interval, }); - const taskPollingLifecycle = new TaskPollingLifecycle({ + this.taskPollingLifecycle = new TaskPollingLifecycle({ config: this.config!, definitions: this.definitions, logger: this.logger, taskStore, middleware: this.middleware, + elasticsearchAndSOAvailability$: this.elasticsearchAndSOAvailability$!, ...managedConfiguration, }); - this.taskPollingLifecycle = taskPollingLifecycle; createMonitoringStats( - taskPollingLifecycle, + this.taskPollingLifecycle, taskStore, + this.elasticsearchAndSOAvailability$!, this.config!, managedConfiguration, this.logger @@ -137,12 +149,9 @@ export class TaskManagerPlugin logger: this.logger, taskStore, middleware: this.middleware, - taskPollingLifecycle, + taskPollingLifecycle: this.taskPollingLifecycle, }); - // start polling for work - taskPollingLifecycle.start(); - return { fetch: (opts: SearchOpts): Promise => taskStore.fetch(opts), get: (id: string) => taskStore.get(id), @@ -153,12 +162,6 @@ export class TaskManagerPlugin }; } - public stop() { - if (this.taskPollingLifecycle) { - this.taskPollingLifecycle.stop(); - } - } - /** * Ensures task manager hasn't started * @@ -171,3 +174,16 @@ export class TaskManagerPlugin } } } + +export function getElasticsearchAndSOAvailability( + core$: Observable +): Observable { + return core$.pipe( + map( + ({ elasticsearch, savedObjects }) => + elasticsearch.level === ServiceStatusLevels.available && + savedObjects.level === ServiceStatusLevels.available + ), + distinctUntilChanged() + ); +} diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts index 9df1e06165bc6..286e29194d6e6 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts @@ -10,7 +10,6 @@ import { of, Observable } from 'rxjs'; export const taskPollingLifecycleMock = { create(opts: { isStarted?: boolean; events$?: Observable }) { return ({ - start: jest.fn(), attemptToRun: jest.fn(), get isStarted() { return opts.isStarted ?? true; @@ -18,7 +17,6 @@ export const taskPollingLifecycleMock = { get events() { return opts.events$ ?? of(); }, - stop: jest.fn(), } as unknown) as jest.Mocked; }, }; diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 5f2e774177fd4..0f807976970cf 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import sinon from 'sinon'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { TaskPollingLifecycle, claimAvailableTasks } from './polling_lifecycle'; import { createInitialMiddleware } from './lib/middleware'; @@ -55,15 +55,64 @@ describe('TaskPollingLifecycle', () => { afterEach(() => clock.restore()); describe('start', () => { - test('begins polling once start is called', () => { - const taskManager = new TaskPollingLifecycle(taskManagerOpts); + test('begins polling once the ES and SavedObjects services are available', () => { + const elasticsearchAndSOAvailability$ = new Subject(); + new TaskPollingLifecycle({ + elasticsearchAndSOAvailability$, + ...taskManagerOpts, + }); + + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).not.toHaveBeenCalled(); + + elasticsearchAndSOAvailability$.next(true); + + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); + }); + }); + + describe('stop', () => { + test('stops polling once the ES and SavedObjects services become unavailable', () => { + const elasticsearchAndSOAvailability$ = new Subject(); + new TaskPollingLifecycle({ + elasticsearchAndSOAvailability$, + ...taskManagerOpts, + }); + + elasticsearchAndSOAvailability$.next(true); + + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); + elasticsearchAndSOAvailability$.next(false); + + mockTaskStore.claimAvailableTasks.mockClear(); clock.tick(150); expect(mockTaskStore.claimAvailableTasks).not.toHaveBeenCalled(); + }); + + test('restarts polling once the ES and SavedObjects services become available again', () => { + const elasticsearchAndSOAvailability$ = new Subject(); + new TaskPollingLifecycle({ + elasticsearchAndSOAvailability$, + ...taskManagerOpts, + }); + + elasticsearchAndSOAvailability$.next(true); - taskManager.start(); + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); + elasticsearchAndSOAvailability$.next(false); + mockTaskStore.claimAvailableTasks.mockClear(); clock.tick(150); + + expect(mockTaskStore.claimAvailableTasks).not.toHaveBeenCalled(); + + elasticsearchAndSOAvailability$.next(true); + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index ba19cb63fffa2..ccba750401f28 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -48,6 +48,7 @@ export type TaskPollingLifecycleOpts = { taskStore: TaskStore; config: TaskManagerConfig; middleware: Middleware; + elasticsearchAndSOAvailability$: Observable; } & ManagedConfiguration; export type TaskLifecycleEvent = @@ -72,8 +73,6 @@ export class TaskPollingLifecycle { private events$ = new Subject(); // all on-demand requests we wish to pipe into the poller private claimRequests$ = new Subject>(); - // the task poller that polls for work on fixed intervals and on demand - private poller$: Observable>>; // our subscription to the poller private pollingSubscription: Subscription = Subscription.EMPTY; @@ -84,36 +83,50 @@ export class TaskPollingLifecycle { * enabling the task manipulation methods, and beginning the background polling * mechanism. */ - constructor(opts: TaskPollingLifecycleOpts) { - const { logger, middleware, maxWorkersConfiguration$, pollIntervalConfiguration$ } = opts; + constructor({ + logger, + middleware, + maxWorkersConfiguration$, + pollIntervalConfiguration$, + // Elasticsearch and SavedObjects availability status + elasticsearchAndSOAvailability$, + config, + taskStore, + definitions, + }: TaskPollingLifecycleOpts) { this.logger = logger; this.middleware = middleware; + this.definitions = definitions; + this.store = taskStore; - this.definitions = opts.definitions; - this.store = opts.taskStore; // pipe store events into the lifecycle event stream this.store.events.subscribe((event) => this.events$.next(event)); this.bufferedStore = new BufferedTaskStore(this.store, { - bufferMaxOperations: opts.config.max_workers, - logger: this.logger, + bufferMaxOperations: config.max_workers, + logger, }); this.pool = new TaskPool({ - logger: this.logger, + logger, maxWorkers$: maxWorkersConfiguration$, }); const { max_poll_inactivity_cycles: maxPollInactivityCycles, poll_interval: pollInterval, - } = opts.config; - this.poller$ = createObservableMonitor>, Error>( + } = config; + + // the task poller that polls for work on fixed intervals and on demand + const poller$: Observable + >> = createObservableMonitor>, Error>( () => createTaskPoller({ - logger: this.logger, + logger, pollInterval$: pollIntervalConfiguration$, - bufferCapacity: opts.config.request_capacity, + bufferCapacity: config.request_capacity, getCapacity: () => this.pool.availableWorkers, pollRequests$: this.claimRequests$, work: this.pollForWork, @@ -133,10 +146,20 @@ export class TaskPollingLifecycle { // operation than just timing out the `work` internally) inactivityTimeout: pollInterval * (maxPollInactivityCycles + 1), onError: (error) => { - this.logger.error(`[Task Poller Monitor]: ${error.message}`); + logger.error(`[Task Poller Monitor]: ${error.message}`); }, } ); + + elasticsearchAndSOAvailability$.subscribe((areESAndSOAvailable) => { + if (areESAndSOAvailable && !this.isStarted) { + // start polling for work + this.pollingSubscription = this.subscribeToPoller(poller$); + } else if (!areESAndSOAvailable && this.isStarted) { + this.pollingSubscription.unsubscribe(); + this.pool.cancelRunningTasks(); + } + }); } public get events(): Observable { @@ -184,39 +207,24 @@ export class TaskPollingLifecycle { ); }; - /** - * Starts up the task manager and starts picking up tasks. - */ - public start() { - if (!this.isStarted) { - this.pollingSubscription = this.poller$ - .pipe( - tap( - mapErr((error: PollingError) => { - if (error.type === PollingErrorType.RequestCapacityReached) { - pipe( - error.data, - mapOptional((id) => this.emitEvent(asTaskRunRequestEvent(id, asErr(error)))) - ); - } - this.logger.error(error.message); - }) - ) + private subscribeToPoller(poller$: Observable>>) { + return poller$ + .pipe( + tap( + mapErr((error: PollingError) => { + if (error.type === PollingErrorType.RequestCapacityReached) { + pipe( + error.data, + mapOptional((id) => this.emitEvent(asTaskRunRequestEvent(id, asErr(error)))) + ); + } + this.logger.error(error.message); + }) ) - .subscribe((event: Result>) => { - this.emitEvent(asTaskPollingCycleEvent(event)); - }); - } - } - - /** - * Stops the task manager and cancels running tasks. - */ - public stop() { - if (this.isStarted) { - this.pollingSubscription.unsubscribe(); - this.pool.cancelRunningTasks(); - } + ) + .subscribe((event: Result>) => { + this.emitEvent(asTaskPollingCycleEvent(event)); + }); } } From d6b006ff2f7383e31c2f0ace0e91ca2f4eea007f Mon Sep 17 00:00:00 2001 From: ymao1 Date: Thu, 29 Oct 2020 07:40:18 -0400 Subject: [PATCH 20/73] [Alerting UI] Removing beta labels (#81919) * Removing beta labels * i18n fix * Fixing test --- .../translations/translations/ja-JP.json | 9 ------ .../translations/translations/zh-CN.json | 9 ------ .../public/application/home.tsx | 17 ----------- .../connector_add_flyout.tsx | 30 ------------------- .../connector_add_modal.tsx | 17 +---------- .../connector_edit_flyout.tsx | 29 ------------------ .../components/alert_details.test.tsx | 26 +--------------- .../components/alert_details.tsx | 17 ----------- .../sections/alert_form/alert_add.tsx | 24 +-------------- .../sections/alert_form/alert_edit.tsx | 16 ---------- .../apps/triggers_actions_ui/home_page.ts | 2 +- 11 files changed, 4 insertions(+), 192 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 58ee36507cb73..1492c8a03906a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20199,7 +20199,6 @@ "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "エンティティ", "xpack.triggersActionsUI.home.alertsTabTitle": "アラート", "xpack.triggersActionsUI.home.appTitle": "アラートとアクション", - "xpack.triggersActionsUI.home.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.home.breadcrumbTitle": "アラートとアクション", "xpack.triggersActionsUI.home.connectorsTabTitle": "コネクター", "xpack.triggersActionsUI.home.sectionDescription": "アラートを使用して条件を検出し、コネクターを使用してアクションを実行します。", @@ -20250,18 +20249,14 @@ "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "時間フィールドが必要です。", "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。", "xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "用語フィールドが必要です。", - "xpack.triggersActionsUI.sections.addConnectorForm.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} コネクタ", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "コネクターを作成できません。", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", - "xpack.triggersActionsUI.sections.addFlyout.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", - "xpack.triggersActionsUI.sections.addModalConnectorForm.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} コネクター", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", - "xpack.triggersActionsUI.sections.alertAdd.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "条件を定義してください", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "アラートの作成", @@ -20291,7 +20286,6 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status": "ステータス", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "アクティブ", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "OK", - "xpack.triggersActionsUI.sections.alertDetails.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle": "無効にする", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "閉じる", @@ -20299,7 +20293,6 @@ "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertInstanceSummaryMessage": "アラートインスタンス概要を読み込めません:{message}", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "アラートを読み込めません: {message}", "xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel": "アプリで表示", - "xpack.triggersActionsUI.sections.alertEdit.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle": "このアラートには無効なアクションがあります", "xpack.triggersActionsUI.sections.alertEdit.flyoutTitle": "アラートを編集", @@ -20412,7 +20405,6 @@ "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel": "件名", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel": "ユーザー名", "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}", - "xpack.triggersActionsUI.sections.editConnectorForm.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "このコネクターは読み取り専用です。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "コネクターを編集", @@ -20422,7 +20414,6 @@ "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "構成", "xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText": "コネクターを更新できません。", "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "「{connectorName}」を更新しました", - "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.betaBadgeTooltipContent": "{pluginName}はベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャルGA機能のSLAが適用されません。", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle": "{connectorName}", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "このコネクターはあらかじめ構成されているため、編集できません。", "xpack.triggersActionsUI.sections.testConnectorForm.awaitingExecutionDescription": "アクションを実行すると、結果がここに表示されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 832b716934bca..be5bcd3cf0543 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20218,7 +20218,6 @@ "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "当实体", "xpack.triggersActionsUI.home.alertsTabTitle": "告警", "xpack.triggersActionsUI.home.appTitle": "告警和操作", - "xpack.triggersActionsUI.home.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.home.breadcrumbTitle": "告警和操作", "xpack.triggersActionsUI.home.connectorsTabTitle": "连接器", "xpack.triggersActionsUI.home.sectionDescription": "使用告警检测条件,并使用连接器采取操作。", @@ -20270,18 +20269,14 @@ "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "时间字段必填。", "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", "xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "词字段必填。", - "xpack.triggersActionsUI.sections.addConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.triggersActionsUI.sections.addFlyout.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", - "xpack.triggersActionsUI.sections.addModalConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.triggersActionsUI.sections.alertAdd.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", @@ -20311,7 +20306,6 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status": "状态", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "活动", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "确定", - "xpack.triggersActionsUI.sections.alertDetails.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle": "禁用", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "关闭", @@ -20319,7 +20313,6 @@ "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertInstanceSummaryMessage": "无法加载告警实例摘要:{message}", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "无法加载告警:{message}", "xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel": "在应用中查看", - "xpack.triggersActionsUI.sections.alertEdit.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle": "此告警具有已禁用的操作", "xpack.triggersActionsUI.sections.alertEdit.flyoutTitle": "编辑告警", @@ -20432,7 +20425,6 @@ "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel": "主题", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel": "用户名", "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}", - "xpack.triggersActionsUI.sections.editConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "此连接器为只读。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "编辑连接器", @@ -20442,7 +20434,6 @@ "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "配置", "xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText": "无法更新连接器。", "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "已更新“{connectorName}”", - "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle": "{connectorName}", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "这是预配置连接器,无法编辑", "xpack.triggersActionsUI.sections.testConnectorForm.awaitingExecutionDescription": "执行该操作时,结果将显示在此处。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index f009a04d40978..482b38ffc0d68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -16,11 +16,9 @@ import { EuiTab, EuiTabs, EuiTitle, - EuiBetaBadge, EuiText, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { Section, routeToConnectors, routeToAlerts } from './constants'; import { getAlertingSectionBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; @@ -29,7 +27,6 @@ import { hasShowActionsCapability } from './lib/capabilities'; import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list'; import { AlertsList } from './sections/alerts_list/components/alerts_list'; -import { PLUGIN } from './constants/plugin'; import { HealthCheck } from './components/health_check'; import { HealthContextProvider } from './context/health_context'; @@ -91,20 +88,6 @@ export const TriggersActionsUIHome: React.FunctionComponent -   - diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 060a751677de0..2e222884dab50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -17,7 +17,6 @@ import { EuiButtonEmpty, EuiButton, EuiFlyoutBody, - EuiBetaBadge, EuiCallOut, EuiSpacer, } from '@elastic/eui'; @@ -31,7 +30,6 @@ import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; -import { PLUGIN } from '../../constants/plugin'; export interface ConnectorAddFlyoutProps { addFlyoutVisible: boolean; @@ -189,20 +187,6 @@ export const ConnectorAddFlyout = ({ actionTypeName: actionType.name, }} /> -   - @@ -216,20 +200,6 @@ export const ConnectorAddFlyout = ({ defaultMessage="Select a connector" id="xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle" /> -   - )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 90abb986517d4..d7ca91218d4dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup, EuiBetaBadge } from '@elastic/eui'; +import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup } from '@elastic/eui'; import { EuiModal, EuiButton, @@ -24,7 +24,6 @@ import { connectorReducer } from './connector_reducer'; import { createActionConnector } from '../../lib/action_connector_api'; import { TypeRegistry } from '../../type_registry'; import './connector_add_modal.scss'; -import { PLUGIN } from '../../constants/plugin'; import { hasSaveActionsCapability } from '../../lib/capabilities'; interface ConnectorAddModalProps { @@ -135,20 +134,6 @@ export const ConnectorAddModal = ({ actionTypeName: actionType.name, }} /> -   -
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 4d8981f25aedc..e89eb8c95fbab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -31,7 +31,6 @@ import { connectorReducer } from './connector_reducer'; import { updateActionConnector, executeAction } from '../../lib/action_connector_api'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; -import { PLUGIN } from '../../constants/plugin'; import { ActionTypeExecutorResult, isActionTypeExecutorResult, @@ -156,20 +155,6 @@ export const ConnectorEditFlyout = ({ } )} /> -   - @@ -187,20 +172,6 @@ export const ConnectorEditFlyout = ({ defaultMessage="Edit connector" id="xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle" /> -   - ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 51c3e030f44eb..662db81101eee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -8,18 +8,8 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertDetails } from './alert_details'; import { Alert, ActionType, ValidationResult } from '../../../../types'; -import { - EuiTitle, - EuiBadge, - EuiFlexItem, - EuiSwitch, - EuiBetaBadge, - EuiButtonEmpty, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; import { ViewInApp } from './view_in_app'; -import { PLUGIN } from '../../../constants/plugin'; import { coreMock } from 'src/core/public/mocks'; import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; @@ -104,20 +94,6 @@ describe('alert_details', () => {

{alert.name} -   -

) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 0af01114731a3..1272024557bb6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -22,12 +22,10 @@ import { EuiSwitch, EuiCallOut, EuiSpacer, - EuiBetaBadge, EuiButtonEmpty, EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; @@ -39,7 +37,6 @@ import { } from '../../common/components/with_bulk_alert_api_operations'; import { AlertInstancesRouteWithApi } from './alert_instances_route'; import { ViewInApp } from './view_in_app'; -import { PLUGIN } from '../../../constants/plugin'; import { AlertEdit } from '../../alert_form'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { routeToAlertDetails } from '../../../constants'; @@ -130,20 +127,6 @@ export const AlertDetails: React.FunctionComponent = ({

{alert.name} -   -

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 763462ba6ebf4..89deb4b26f012 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -6,14 +6,7 @@ import React, { useCallback, useReducer, useState, useEffect } from 'react'; import { isObject } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiTitle, - EuiFlyoutHeader, - EuiFlyout, - EuiFlyoutBody, - EuiPortal, - EuiBetaBadge, -} from '@elastic/eui'; +import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; import { Alert, AlertAction, IErrorObject } from '../../../types'; @@ -21,7 +14,6 @@ import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; -import { PLUGIN } from '../../constants/plugin'; import { ConfirmAlertSave } from './confirm_alert_save'; import { hasShowActionsCapability } from '../../lib/capabilities'; import AlertAddFooter from './alert_add_footer'; @@ -163,20 +155,6 @@ export const AlertAdd = ({ defaultMessage="Create alert" id="xpack.triggersActionsUI.sections.alertAdd.flyoutTitle" /> -   - diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 0435a4cc33cb8..5eadc742a9dc8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -16,7 +16,6 @@ import { EuiButton, EuiFlyoutBody, EuiPortal, - EuiBetaBadge, EuiCallOut, EuiSpacer, } from '@elastic/eui'; @@ -27,7 +26,6 @@ import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; -import { PLUGIN } from '../../constants/plugin'; import { HealthContextProvider } from '../../context/health_context'; interface AlertEditProps { @@ -119,20 +117,6 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { defaultMessage="Edit alert" id="xpack.triggersActionsUI.sections.alertEdit.flyoutTitle" /> -   - diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 3b93607832670..bd799947256d6 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await log.debug('Checking for section heading to say Triggers and Actions.'); const headingText = await pageObjects.triggersActionsUI.getSectionHeadingText(); - expect(headingText).to.be('Alerts and Actions BETA'); + expect(headingText).to.be('Alerts and Actions'); }); describe('Connectors tab', () => { From db92edff1fad494819b542d911b7b7b53bd27f14 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 29 Oct 2020 12:46:01 +0100 Subject: [PATCH 21/73] [UX] Create apm static index pattern on ux page visit (#81842) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/public/application/csmApp.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 2baddbe572a52..d8f54c7bfc94f 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -28,6 +28,7 @@ import { ConfigSchema } from '../index'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; +import { createStaticIndexPattern } from '../services/rest/index_pattern'; const CsmMainContainer = styled.div` padding: ${px(units.plus)}; @@ -114,6 +115,12 @@ export const renderApp = ( ) => { createCallApmApi(core.http); + // Automatically creates static index pattern and stores as saved object + createStaticIndexPattern().catch((e) => { + // eslint-disable-next-line no-console + console.log('Error creating static index pattern', e); + }); + ReactDOM.render( Date: Thu, 29 Oct 2020 14:51:09 +0300 Subject: [PATCH 22/73] Vega visualization renderer (#81606) * Create vega to_ast function * Create a custom vega renderer * Fix sass error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vega_editor.scss} | 0 .../vega_vis.scss} | 8 ++ .../public/components/vega_vis_component.tsx | 85 +++++++++++++++++++ .../public/components/vega_vis_editor.tsx | 6 +- .../components/vega_vis_editor_lazy.tsx | 29 +++++++ src/plugins/vis_type_vega/public/index.scss | 9 -- src/plugins/vis_type_vega/public/plugin.ts | 3 +- src/plugins/vis_type_vega/public/to_ast.ts | 32 +++++++ src/plugins/vis_type_vega/public/vega_fn.ts | 14 +-- .../vega_inspector/vega_data_inspector.tsx | 6 +- .../public/vega_inspector/vega_inspector.tsx | 13 ++- src/plugins/vis_type_vega/public/vega_type.ts | 15 ++-- .../public/vega_view/vega_base_view.d.ts | 40 +++++++++ .../public/vega_view/vega_base_view.js | 29 ++++--- .../public/vega_view/vega_map_view.d.ts | 22 +++++ .../index.ts => vega_view/vega_view.d.ts} | 4 +- .../public/vega_vis_renderer.tsx | 51 +++++++++++ .../public/vega_visualization.test.js | 22 +---- ...visualization.js => vega_visualization.ts} | 59 +++++++------ .../__snapshots__/build_pipeline.test.ts.snap | 2 - .../public/legacy/build_pipeline.test.ts | 8 -- .../public/legacy/build_pipeline.ts | 3 - 22 files changed, 354 insertions(+), 106 deletions(-) rename src/plugins/vis_type_vega/public/{_vega_editor.scss => components/vega_editor.scss} (100%) rename src/plugins/vis_type_vega/public/{_vega_vis.scss => components/vega_vis.scss} (96%) create mode 100644 src/plugins/vis_type_vega/public/components/vega_vis_component.tsx create mode 100644 src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx delete mode 100644 src/plugins/vis_type_vega/public/index.scss create mode 100644 src/plugins/vis_type_vega/public/to_ast.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts rename src/plugins/vis_type_vega/public/{components/index.ts => vega_view/vega_view.d.ts} (89%) create mode 100644 src/plugins/vis_type_vega/public/vega_vis_renderer.tsx rename src/plugins/vis_type_vega/public/{vega_visualization.js => vega_visualization.ts} (70%) diff --git a/src/plugins/vis_type_vega/public/_vega_editor.scss b/src/plugins/vis_type_vega/public/components/vega_editor.scss similarity index 100% rename from src/plugins/vis_type_vega/public/_vega_editor.scss rename to src/plugins/vis_type_vega/public/components/vega_editor.scss diff --git a/src/plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/components/vega_vis.scss similarity index 96% rename from src/plugins/vis_type_vega/public/_vega_vis.scss rename to src/plugins/vis_type_vega/public/components/vega_vis.scss index 6a0d20246089e..004eed5a3972b 100644 --- a/src/plugins/vis_type_vega/public/_vega_vis.scss +++ b/src/plugins/vis_type_vega/public/components/vega_vis.scss @@ -1,3 +1,11 @@ +.vgaVis__wrapper { + @include euiScrollBar; + + display: flex; + flex: 1 1 0; + overflow: auto; +} + .vgaVis { display: flex; flex: 1 1 100%; diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_component.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_component.tsx new file mode 100644 index 0000000000000..f1a2f8cbfa8d4 --- /dev/null +++ b/src/plugins/vis_type_vega/public/components/vega_vis_component.tsx @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useMemo, useRef } from 'react'; +import { EuiResizeObserver } from '@elastic/eui'; +import { throttle } from 'lodash'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { createVegaVisualization } from '../vega_visualization'; +import { VegaVisualizationDependencies } from '../plugin'; +import { VegaParser } from '../data_model/vega_parser'; + +import './vega_vis.scss'; + +interface VegaVisComponentProps { + deps: VegaVisualizationDependencies; + fireEvent: IInterpreterRenderHandlers['event']; + renderComplete: () => void; + visData: VegaParser; +} + +type VegaVisController = InstanceType>; + +const VegaVisComponent = ({ visData, fireEvent, renderComplete, deps }: VegaVisComponentProps) => { + const chartDiv = useRef(null); + const visController = useRef(null); + + useEffect(() => { + if (chartDiv.current) { + const VegaVis = createVegaVisualization(deps); + visController.current = new VegaVis(chartDiv.current, fireEvent); + } + + return () => { + visController.current?.destroy(); + visController.current = null; + }; + }, [deps, fireEvent]); + + useEffect(() => { + if (visController.current) { + visController.current.render(visData).then(renderComplete); + } + }, [visData, renderComplete]); + + const updateChartSize = useMemo( + () => + throttle(() => { + if (visController.current) { + visController.current.render(visData).then(renderComplete); + } + }, 300), + [renderComplete, visData] + ); + + return ( + + {(resizeRef) => ( +
+
+
+ )} + + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VegaVisComponent as default }; diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 5e770fcff556d..7d669235c36b8 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -30,6 +30,8 @@ import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; import { VegaActionsMenu } from './vega_actions_menu'; +import './vega_editor.scss'; + const aceOptions = { maxLines: Infinity, highlightActiveLine: false, @@ -102,4 +104,6 @@ function VegaVisEditor({ stateParams, setValue }: VisOptionsProps) { ); } -export { VegaVisEditor }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VegaVisEditor as default }; diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx new file mode 100644 index 0000000000000..d6c78972410e0 --- /dev/null +++ b/src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { VisParams } from '../vega_fn'; + +const VegaVisEditor = lazy(() => import('./vega_vis_editor')); + +export const VegaVisEditorComponent = (props: VisOptionsProps) => ( + +); diff --git a/src/plugins/vis_type_vega/public/index.scss b/src/plugins/vis_type_vega/public/index.scss deleted file mode 100644 index 78d9eb61999f7..0000000000000 --- a/src/plugins/vis_type_vega/public/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Prefix all styles with "vga" to avoid conflicts. -// Examples -// vgaChart -// vgaChart__legend -// vgaChart__legend--small -// vgaChart__legend-isLoading - -@import './vega_vis'; -@import './vega_editor'; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index ce5c5130961c6..04481685c841b 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -35,10 +35,10 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; import { IServiceSettings } from '../../maps_legacy/public'; -import './index.scss'; import { ConfigSchema } from '../config'; import { getVegaInspectorView } from './vega_inspector'; +import { getVegaVisRenderer } from './vega_vis_renderer'; /** @internal */ export interface VegaVisualizationDependencies { @@ -93,6 +93,7 @@ export class VegaPlugin implements Plugin, void> { inspector.registerView(getVegaInspectorView({ uiSettings: core.uiSettings })); expressions.registerFunction(() => createVegaFn(visualizationDependencies)); + expressions.registerRenderer(getVegaVisRenderer(visualizationDependencies)); visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); } diff --git a/src/plugins/vis_type_vega/public/to_ast.ts b/src/plugins/vis_type_vega/public/to_ast.ts new file mode 100644 index 0000000000000..a5fe8f13c3daf --- /dev/null +++ b/src/plugins/vis_type_vega/public/to_ast.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { Vis } from '../../visualizations/public'; +import { VegaExpressionFunctionDefinition, VisParams } from './vega_fn'; + +export const toExpressionAst = (vis: Vis) => { + const vega = buildExpressionFunction('vega', { + spec: vis.params.spec, + }); + + const ast = buildExpression([vega]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index c88b78948133c..25d4e76c336b3 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -40,21 +40,23 @@ interface Arguments { export type VisParams = Required; -interface RenderValue { +export interface RenderValue { visData: VegaParser; visType: 'vega'; visConfig: VisParams; } -export const createVegaFn = ( - dependencies: VegaVisualizationDependencies -): ExpressionFunctionDefinition< +export type VegaExpressionFunctionDefinition = ExpressionFunctionDefinition< 'vega', Input, Arguments, Output, ExecutionContext -> => ({ +>; + +export const createVegaFn = ( + dependencies: VegaVisualizationDependencies +): VegaExpressionFunctionDefinition => ({ name: 'vega', type: 'render', inputTypes: ['kibana_context', 'null'], @@ -80,7 +82,7 @@ export const createVegaFn = ( return { type: 'render', - as: 'visualization', + as: 'vega_vis', value: { visData: response, visType: 'vega', diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx index 6dfa7a23c4fe8..350e781dc7076 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx @@ -41,7 +41,7 @@ const specLabel = i18n.translate('visTypeVega.inspector.specLabel', { defaultMessage: 'Spec', }); -export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { +const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { const tabs = [ { id: 'data-viewer--id', @@ -75,3 +75,7 @@ export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { /> ); }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VegaDataInspector as default }; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx index 83d9e467646a6..4b3e48d6a37a0 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx @@ -16,14 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/public'; -import { VegaAdapter } from './vega_adapter'; -import { VegaDataInspector, VegaDataInspectorProps } from './vega_data_inspector'; import { KibanaContextProvider } from '../../../kibana_react/public'; import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../inspector/public'; +import { VegaAdapter } from './vega_adapter'; +import type { VegaDataInspectorProps } from './vega_data_inspector'; + +const VegaDataInspector = lazy(() => import('./vega_data_inspector')); export interface VegaInspectorAdapters extends Adapters { requests: RequestAdapter; @@ -46,7 +49,9 @@ export const getVegaInspectorView = (dependencies: VegaInspectorViewDependencies }, component: (props) => ( - + }> + + ), } as InspectorViewDescription); diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 0496f765e5e99..17f35b75f0016 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -21,22 +21,20 @@ import { i18n } from '@kbn/i18n'; import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; -import { VegaVisEditor } from './components'; import { createVegaRequestHandler } from './vega_request_handler'; -// @ts-expect-error -import { createVegaVisualization } from './vega_visualization'; import { getDefaultSpec } from './default_spec'; import { createInspectorAdapters } from './vega_inspector'; import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; - +import { toExpressionAst } from './to_ast'; +import { VisParams } from './vega_fn'; import { getInfoMessage } from './components/experimental_map_vis_info'; +import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; export const createVegaTypeDefinition = ( dependencies: VegaVisualizationDependencies -): BaseVisTypeOptions => { +): BaseVisTypeOptions => { const requestHandler = createVegaRequestHandler(dependencies); - const visualization = createVegaVisualization(dependencies); return { name: 'vega', @@ -49,13 +47,12 @@ export const createVegaTypeDefinition = ( icon: 'visVega', visConfig: { defaults: { spec: getDefaultSpec() } }, editorConfig: { - optionsTemplate: VegaVisEditor, + optionsTemplate: VegaVisEditorComponent, enableAutoApply: true, defaultSize: DefaultEditorSize.MEDIUM, }, - visualization, requestHandler, - responseHandler: 'none', + toExpressionAst, options: { showIndexSelection: false, showQueryBar: true, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts new file mode 100644 index 0000000000000..54b96813769ba --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { IServiceSettings } from 'src/plugins/maps_legacy/public'; +import { VegaParser } from '../data_model/vega_parser'; + +interface VegaViewParams { + parentEl: HTMLDivElement; + fireEvent: IInterpreterRenderHandlers['event']; + vegaParser: VegaParser; + serviceSettings: IServiceSettings; + filterManager: DataPublicPluginStart['query']['filterManager']; + timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; + // findIndex: (index: string) => Promise<...>; +} + +export class VegaBaseView { + constructor(params: VegaViewParams); + init(): Promise; + onError(error: any): void; + destroy(): Promise; +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 979432b2aed2a..25ea77ddbccb4 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -63,7 +63,7 @@ export class VegaBaseView { this._parser = opts.vegaParser; this._serviceSettings = opts.serviceSettings; this._filterManager = opts.filterManager; - this._applyFilter = opts.applyFilter; + this._fireEvent = opts.fireEvent; this._timefilter = opts.timefilter; this._findIndex = opts.findIndex; this._view = null; @@ -264,7 +264,7 @@ export class VegaBaseView { const indexId = await this._findIndex(index); const filter = esFilters.buildQueryFilter(query, indexId); - this._applyFilter({ filters: [filter] }); + this._fireEvent({ name: 'applyFilter', data: { filters: [filter] } }); } /** @@ -301,19 +301,22 @@ export class VegaBaseView { setTimeFilterHandler(start, end) { const { from, to, mode } = VegaBaseView._parseTimeRange(start, end); - this._applyFilter({ - timeFieldName: '*', - filters: [ - { - range: { - '*': { - mode, - gte: from, - lte: to, + this._fireEvent({ + name: 'applyFilter', + data: { + timeFieldName: '*', + filters: [ + { + range: { + '*': { + mode, + gte: from, + lte: to, + }, }, }, - }, - ], + ], + }, }); } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts new file mode 100644 index 0000000000000..a1210e05f4507 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VegaBaseView } from './vega_base_view'; + +export class VegaMapView extends VegaBaseView {} diff --git a/src/plugins/vis_type_vega/public/components/index.ts b/src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts similarity index 89% rename from src/plugins/vis_type_vega/public/components/index.ts rename to src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts index 90f067c778fd2..c137d8222750c 100644 --- a/src/plugins/vis_type_vega/public/components/index.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts @@ -17,4 +17,6 @@ * under the License. */ -export { VegaVisEditor } from './vega_vis_editor'; +import { VegaBaseView } from './vega_base_view'; + +export class VegaView extends VegaBaseView {} diff --git a/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx b/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx new file mode 100644 index 0000000000000..542f59b3dfff9 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { VisualizationContainer } from '../../visualizations/public'; +import { VegaVisualizationDependencies } from './plugin'; +import { RenderValue } from './vega_fn'; +const VegaVisComponent = lazy(() => import('./components/vega_vis_component')); + +export const getVegaVisRenderer: ( + deps: VegaVisualizationDependencies +) => ExpressionRenderDefinition = (deps) => ({ + name: 'vega_vis', + reuseDomNode: true, + render: (domNode, { visData }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index dcf1722768075..837fdf2a9aea3 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -30,8 +30,6 @@ import vegaMapGraph from './test_utils/vega_map_test.json'; import { VegaParser } from './data_model/vega_parser'; import { SearchAPI } from './data_model/search_api'; -import { createVegaTypeDefinition } from './vega_type'; - import { setInjectedVars, setData, setSavedObjects, setNotifications } from './services'; import { coreMock } from '../../../core/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; @@ -49,9 +47,7 @@ jest.mock('./lib/vega', () => ({ describe('VegaVisualizations', () => { let domNode; let VegaVisualization; - let vis; let vegaVisualizationDependencies; - let vegaVisType; let mockWidth; let mockedWidthValue; @@ -91,22 +87,12 @@ describe('VegaVisualizations', () => { getServiceSettings: mockGetServiceSettings, }; - vegaVisType = createVegaTypeDefinition(vegaVisualizationDependencies); VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); }); describe('VegaVisualization - basics', () => { beforeEach(async () => { setupDOM(); - - vis = { - type: vegaVisType, - API: { - events: { - applyFilter: jest.fn(), - }, - }, - }; }); afterEach(() => { @@ -117,7 +103,7 @@ describe('VegaVisualizations', () => { test('should show vegalite graph and update on resize (may fail in dev env)', async () => { let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); + vegaVis = new VegaVisualization(domNode, jest.fn()); const vegaParser = new VegaParser( JSON.stringify(vegaliteGraph), @@ -137,7 +123,7 @@ describe('VegaVisualizations', () => { mockedWidthValue = 256; mockedHeightValue = 256; - await vegaVis._vegaView.resize(); + await vegaVis.vegaView.resize(); expect(domNode.innerHTML).toMatchSnapshot(); } finally { @@ -148,7 +134,7 @@ describe('VegaVisualizations', () => { test('should show vega graph (may fail in dev env)', async () => { let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); + vegaVis = new VegaVisualization(domNode, jest.fn()); const vegaParser = new VegaParser( JSON.stringify(vegaGraph), new SearchAPI({ @@ -172,7 +158,7 @@ describe('VegaVisualizations', () => { test('should show vega blank rectangle on top of a map (vegamap)', async () => { let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); + vegaVis = new VegaVisualization(domNode, jest.fn()); const vegaParser = new VegaParser( JSON.stringify(vegaMapGraph), new SearchAPI({ diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.ts similarity index 70% rename from src/plugins/vis_type_vega/public/vega_visualization.js rename to src/plugins/vis_type_vega/public/vega_visualization.ts index 2d58e9cda60cd..58c436bcd4be4 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.ts @@ -17,28 +17,34 @@ * under the License. */ import { i18n } from '@kbn/i18n'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { VegaParser } from './data_model/vega_parser'; +import { VegaVisualizationDependencies } from './plugin'; import { getNotifications, getData, getSavedObjects } from './services'; +import type { VegaView } from './vega_view/vega_view'; -export const createVegaVisualization = ({ getServiceSettings }) => +export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizationDependencies) => class VegaVisualization { - constructor(el, vis) { - this._el = el; - this._vis = vis; + private readonly dataPlugin = getData(); + private readonly savedObjectsClient = getSavedObjects(); + private vegaView: InstanceType | null = null; - this.savedObjectsClient = getSavedObjects(); - this.dataPlugin = getData(); - } + constructor( + private el: HTMLDivElement, + private fireEvent: IInterpreterRenderHandlers['event'] + ) {} /** * Find index pattern by its title, of if not given, gets default * @param {string} [index] * @returns {Promise} index id */ - async findIndex(index) { + async findIndex(index: string) { const { indexPatterns } = this.dataPlugin; let idxObj; if (index) { + // @ts-expect-error idxObj = indexPatterns.findByTitle(this.savedObjectsClient, index); if (!idxObj) { throw new Error( @@ -61,16 +67,10 @@ export const createVegaVisualization = ({ getServiceSettings }) => return idxObj.id; } - /** - * - * @param {VegaParser} visData - * @param {*} status - * @returns {Promise} - */ - async render(visData) { + async render(visData: VegaParser) { const { toasts } = getNotifications(); - if (!visData && !this._vegaView) { + if (!visData && !this.vegaView) { toasts.addWarning( i18n.translate('visTypeVega.visualization.unableToRenderWithoutDataWarningMessage', { defaultMessage: 'Unable to render without data', @@ -82,8 +82,8 @@ export const createVegaVisualization = ({ getServiceSettings }) => try { await this._render(visData); } catch (error) { - if (this._vegaView) { - this._vegaView.onError(error); + if (this.vegaView) { + this.vegaView.onError(error); } else { toasts.addError(error, { title: i18n.translate('visTypeVega.visualization.renderErrorTitle', { @@ -94,20 +94,20 @@ export const createVegaVisualization = ({ getServiceSettings }) => } } - async _render(vegaParser) { + async _render(vegaParser: VegaParser) { if (vegaParser) { // New data received, rebuild the graph - if (this._vegaView) { - await this._vegaView.destroy(); - this._vegaView = null; + if (this.vegaView) { + await this.vegaView.destroy(); + this.vegaView = null; } const serviceSettings = await getServiceSettings(); const { filterManager } = this.dataPlugin.query; const { timefilter } = this.dataPlugin.query.timefilter; const vegaViewParams = { - parentEl: this._el, - applyFilter: this._vis.API.events.applyFilter, + parentEl: this.el, + fireEvent: this.fireEvent, vegaParser, serviceSettings, filterManager, @@ -116,18 +116,17 @@ export const createVegaVisualization = ({ getServiceSettings }) => }; if (vegaParser.useMap) { - const services = { toastService: getNotifications().toasts }; const { VegaMapView } = await import('./vega_view/vega_map_view'); - this._vegaView = new VegaMapView(vegaViewParams, services); + this.vegaView = new VegaMapView(vegaViewParams); } else { - const { VegaView } = await import('./vega_view/vega_view'); - this._vegaView = new VegaView(vegaViewParams); + const { VegaView: VegaViewClass } = await import('./vega_view/vega_view'); + this.vegaView = new VegaViewClass(vegaViewParams); } - await this._vegaView.init(); + await this.vegaView?.init(); } } destroy() { - return this._vegaView && this._vegaView.destroy(); + this.vegaView?.destroy(); } }; diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index cbdecd4aac747..959d9031853af 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -13,5 +13,3 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index c744043ed155b..501a69080d93f 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -94,14 +94,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { uiState = {}; }); - it('handles vega function', () => { - const vis = { - params: { spec: 'this is a test' }, - }; - const actual = buildPipelineVisFunction.vega(vis.params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - it('handles input_control_vis function', () => { const params = { some: 'nested', diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index eb431212166a3..b08583c376b36 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -254,9 +254,6 @@ const adjustVislibDimensionFormmaters = (vis: Vis, dimensions: { y: any[] }): vo }; export const buildPipelineVisFunction: BuildPipelineVisFunction = { - vega: (params) => { - return `vega ${prepareString('spec', params.spec)}`; - }, input_control_vis: (params) => { return `input_control_vis ${prepareJson('visConfig', params)}`; }, From 13fe95b400ea6bd933921d24a6b24313e11be193 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 29 Oct 2020 12:32:36 +0000 Subject: [PATCH 23/73] Enables the EventLog Client to query across ILM versions of the `.event-log` index (#81920) Fixes a bug in the EventLog client which caused it to query for events created in the current version instead of querying across versions. --- .../event_log/server/event_log_client.test.ts | 4 +- .../event_log/server/event_log_client.ts | 2 +- .../event_log_multiple_indicies/data.json | 274 +++++++++ .../event_log_multiple_indicies/mappings.json | 576 ++++++++++++++++++ .../event_log/public_api_integration.ts | 26 + 5 files changed, 879 insertions(+), 3 deletions(-) create mode 100644 x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json create mode 100644 x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index 3273fe847080f..d6793be425585 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -114,7 +114,7 @@ describe('EventLogStart', () => { ).toEqual(result); expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( - esContext.esNames.alias, + esContext.esNames.indexPattern, undefined, 'saved-object-type', 'saved-object-id', @@ -195,7 +195,7 @@ describe('EventLogStart', () => { ).toEqual(result); expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( - esContext.esNames.alias, + esContext.esNames.indexPattern, undefined, 'saved-object-type', 'saved-object-id', diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 32fd99d170026..b7de4acb9428c 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -92,7 +92,7 @@ export class EventLogClient implements IEventLogClient { await this.savedObjectGetter(type, id); return await this.esContext.esAdapter.queryEventsBySavedObject( - this.esContext.esNames.alias, + this.esContext.esNames.indexPattern, namespace, type, id, diff --git a/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json b/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json new file mode 100644 index 0000000000000..4e871f6308b77 --- /dev/null +++ b/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json @@ -0,0 +1,274 @@ +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 9007199254740991 + }, + "migrationVersion": { + "config": "7.9.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2020-10-28T15:19:15.795Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "_reserved": true, + "color": "#00bfb3", + "description": "This is your default space!", + "disabledFeatures": [ + ], + "name": "Default" + }, + "type": "space", + "updated_at": "2020-10-28T15:19:15.857Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:namespace-a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "disabledFeatures": [ + ], + "name": "Space A" + }, + "type": "space", + "updated_at": "2020-10-28T15:19:52.887Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "event_log_test:421f2511-5cd1-44fd-95df-e0df83e354d5", + "index": ".kibana_1", + "source": { + "event_log_test": { + }, + "references": [ + ], + "type": "event_log_test", + "updated_at": "2020-10-28T15:19:53.861Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "XKbLb3UBt6Z_MVvSSPbe", + "index": ".kibana-event-log-7.9.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:54.841Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:54.841Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:54.841Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:53.825Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "XabLb3UBt6Z_MVvSSfYD", + "index": ".kibana-event-log-7.9.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:54.879Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:54.879Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:54.879Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:54.849Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "XqbLb3UBt6Z_MVvSSfYe", + "index": ".kibana-event-log-7.9.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:54.905Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:54.905Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:54.905Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:54.881Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "X6bLb3UBt6Z_MVvSTfYk", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.933Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.933Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.933Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:55.913Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "YKbLb3UBt6Z_MVvSTfY8", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.957Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.957Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.957Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:55.938Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "YabLb3UBt6Z_MVvSTfZc", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.991Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.991Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.991Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:55.962Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json b/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json new file mode 100644 index 0000000000000..b418ccc1343a2 --- /dev/null +++ b/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json @@ -0,0 +1,576 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "eaf6f5841dbf4cb5e3045860f75f53ca", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "event_log_test": "bef808d4a9c27f204ffbda3359233931", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "f74dfe498e1849267cda41580b2be110", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "event_log_test": { + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana-event-log-7.9.0": { + "is_write_index": true + } + }, + "index": ".kibana-event-log-7.9.0-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "message": { + "norms": false, + "type": "text" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + } + } + }, + "kibana": { + "properties": { + "alerting": { + "properties": { + "instance_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "saved_objects": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "rel": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "server_uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "tags": { + "ignore_above": 1024, + "meta": { + "isArray": "true" + }, + "type": "keyword" + }, + "user": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "lifecycle": { + "name": "kibana-event-log-policy", + "rollover_alias": ".kibana-event-log-7.9.0" + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana-event-log-8.0.0": { + "is_write_index": true + } + }, + "index": ".kibana-event-log-8.0.0-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "message": { + "norms": false, + "type": "text" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + } + } + }, + "kibana": { + "properties": { + "alerting": { + "properties": { + "instance_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "saved_objects": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "rel": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "server_uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "tags": { + "ignore_above": 1024, + "meta": { + "isArray": "true" + }, + "type": "keyword" + }, + "user": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "lifecycle": { + "name": "kibana-event-log-policy", + "rollover_alias": ".kibana-event-log-8.0.0" + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index eea18863e3be8..dff10daafbdb8 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -157,6 +157,32 @@ export default function ({ getService }: FtrProviderContext) { }); }); } + + describe(`Index Lifecycle`, () => { + it('should query across indicies matching the Event Log index pattern', async () => { + await esArchiver.load('event_log_multiple_indicies'); + + const id = `421f2511-5cd1-44fd-95df-e0df83e354d5`; + + const { + body: { data, total }, + } = await findEvents(undefined, id, {}); + + expect(data.length).to.be(6); + expect(total).to.be(6); + + expect(data.map((foundEvent: IEvent) => foundEvent?.message)).to.eql([ + 'test 2020-10-28T15:19:53.825Z', + 'test 2020-10-28T15:19:54.849Z', + 'test 2020-10-28T15:19:54.881Z', + 'test 2020-10-28T15:19:55.913Z', + 'test 2020-10-28T15:19:55.938Z', + 'test 2020-10-28T15:19:55.962Z', + ]); + + await esArchiver.unload('event_log_multiple_indicies'); + }); + }); }); async function findEvents( From d1344b6ecd294641f5f0f10ca826f3fa0618ae8c Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 29 Oct 2020 12:33:22 +0000 Subject: [PATCH 24/73] added alerting to app directory (#81902) Adds a link to the Alerts & Actions from the app directory --- .../plugins/triggers_actions_ui/kibana.json | 4 +-- .../triggers_actions_ui/public/plugin.ts | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index c9187096821d8..a4446e0a75120 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,9 +3,9 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerts", "stackAlerts"], + "optionalPlugins": ["home", "alerts", "stackAlerts"], "requiredPlugins": ["management", "charts", "data", "kibanaReact"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], - "requiredBundles": ["alerts", "esUiShared"] + "requiredBundles": ["home", "alerts", "esUiShared"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 874a380f56b5f..393ac5bc1b74d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -20,6 +20,10 @@ import { ManagementAppMountParams, ManagementSetup, } from '../../../../src/plugins/management/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; @@ -40,6 +44,7 @@ export interface TriggersAndActionsUIPublicPluginStart { interface PluginsSetup { management: ManagementSetup; + home?: HomePublicPluginSetup; } interface PluginsStart { @@ -73,11 +78,31 @@ export class Plugin const actionTypeRegistry = this.actionTypeRegistry; const alertTypeRegistry = this.alertTypeRegistry; + const featureTitle = i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { + defaultMessage: 'Alerts and Actions', + }); + const featureDescription = i18n.translate( + 'xpack.triggersActionsUI.managementSection.displayDescription', + { + defaultMessage: 'Detect conditions using alerts, and take actions using connectors.', + } + ); + + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: 'triggersActions', + title: featureTitle, + description: featureDescription, + icon: 'watchesApp', + path: '/app/management/insightsAndAlerting/triggersActions', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + } + plugins.management.sections.section.insightsAndAlerting.registerApp({ id: 'triggersActions', - title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { - defaultMessage: 'Alerts and Actions', - }), + title: featureTitle, order: 0, async mount(params: ManagementAppMountParams) { const [coreStart, pluginsStart] = (await core.getStartServices()) as [ From 275c30a926a3bde5836180b726389d73de39addf Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 29 Oct 2020 12:44:51 +0000 Subject: [PATCH 25/73] skip flaky suite (#81632) --- test/functional/apps/discover/_doc_table.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 7fc120f9ea474..d3c0fe834958d 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -88,7 +88,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await footer.getVisibleText()).to.have.string(rowsHardLimit); }); - describe('expand a document row', function () { + // FLAKY: https://github.com/elastic/kibana/issues/81632 + describe.skip('expand a document row', function () { const rowToInspect = 1; beforeEach(async function () { // close the toggle if open From 1407f713e517b51cefcbf2f0a890a56bd9b7c21d Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 29 Oct 2020 14:35:48 +0100 Subject: [PATCH 26/73] Update KibanaRequest to use the new WHATWG URL API (#80713) --- ...kibana-plugin-core-server.kibanarequest.md | 4 +-- ...-core-server.kibanarequest.rewrittenurl.md | 2 +- ...na-plugin-core-server.kibanarequest.url.md | 2 +- src/core/server/http/http_server.mocks.ts | 28 +++++++++++-------- src/core/server/http/http_server.ts | 2 +- .../http/integration_tests/lifecycle.test.ts | 24 ++++++++++++++-- .../server/http/lifecycle/on_pre_routing.ts | 25 +++++++++++++++-- src/core/server/http/router/request.ts | 15 +++++----- src/core/server/server.api.md | 6 ++-- .../server/lib/task_runner_factory.test.ts | 18 ++++++++++++ .../actions/server/lib/task_runner_factory.ts | 6 ++++ .../server/alerts_client_factory.test.ts | 6 ++++ x-pack/plugins/alerts/server/plugin.test.ts | 6 ++++ .../server/task_runner/task_runner.test.ts | 18 ++++++++++++ .../alerts/server/task_runner/task_runner.ts | 6 ++++ ...dashboard_mode_request_interceptor.test.ts | 10 +++---- .../dashboard_mode_request_interceptor.ts | 2 +- .../lib/enterprise_search_config_api.test.ts | 1 - .../event_log/server/event_log_client.test.ts | 6 ++++ .../server/event_log_start_service.test.ts | 6 ++++ .../saved_object_provider_registry.test.ts | 6 ++++ .../server/services/agent_policy_update.ts | 6 ++++ .../agents/checkin/state_connected_agents.ts | 6 ++++ .../agents/checkin/state_new_actions.ts | 6 ++++ .../ml/server/routes/data_frame_analytics.ts | 12 ++------ x-pack/plugins/reporting/server/core.ts | 7 +++++ .../lib/authorized_user_pre_routing.test.ts | 2 +- .../server/audit/audit_events.test.ts | 16 ++++------- .../security/server/audit/audit_events.ts | 14 +++++----- .../server/authentication/authenticator.ts | 4 +-- .../authentication/providers/basic.test.ts | 4 +-- .../server/authentication/providers/basic.ts | 10 +++++-- .../server/authentication/providers/http.ts | 8 ++++-- .../authentication/providers/kerberos.ts | 6 ++-- .../server/authentication/providers/oidc.ts | 8 ++++-- .../server/authentication/providers/pki.ts | 6 ++-- .../server/authentication/providers/saml.ts | 8 ++++-- .../authentication/providers/token.test.ts | 4 +-- .../server/authentication/providers/token.ts | 10 +++++-- .../server/authorization/api_authorization.ts | 6 ++-- .../authorization/authorization_service.tsx | 2 +- .../server/routes/authentication/oidc.ts | 2 +- .../server/routes/views/login.test.ts | 4 +-- .../on_post_auth_interceptor.ts | 2 +- .../on_request_interceptor.ts | 13 ++------- .../spaces_service/spaces_service.test.ts | 10 +++---- 46 files changed, 262 insertions(+), 113 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md index 1134994faa9bd..4129662acb2b1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md @@ -30,9 +30,9 @@ export declare class KibanaRequestboolean | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the HttpFetchOptions#asSystemRequest option. | | [params](./kibana-plugin-core-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-core-server.kibanarequest.query.md) | | Query | | -| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | Url | URL rewritten in onPreRouting request interceptor. | +| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | URL | URL rewritten in onPreRouting request interceptor. | | [route](./kibana-plugin-core-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | | [socket](./kibana-plugin-core-server.kibanarequest.socket.md) | | IKibanaSocket | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | -| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | +| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | URL | a WHATWG URL standard object. | | [uuid](./kibana-plugin-core-server.kibanarequest.uuid.md) | | string | A UUID to identify this request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md index 10628bafaf1d4..fb547330ee6ea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md @@ -9,5 +9,5 @@ URL rewritten in onPreRouting request interceptor. Signature: ```typescript -readonly rewrittenUrl?: Url; +readonly rewrittenUrl?: URL; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md index 31d1348197201..b72760e272bb2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md @@ -9,5 +9,5 @@ a WHATWG URL standard object. Signature: ```typescript -readonly url: Url; +readonly url: URL; ``` diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 6aad232cf42b6..d615e799f383f 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { parse as parseUrl } from 'url'; import { Request } from 'hapi'; import { merge } from 'lodash'; import { Socket } from 'net'; @@ -72,6 +73,7 @@ function createKibanaRequestMock

({ auth = { isAuthenticated: true }, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); + const url = parseUrl(`${path}${queryString ? `?${queryString}` : ''}`); return KibanaRequest.from( createRawRequestMock({ @@ -83,12 +85,7 @@ function createKibanaRequestMock

({ payload: body, path, method, - url: { - path, - pathname: path, - query: queryString, - search: queryString ? `?${queryString}` : queryString, - }, + url, route: { settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteOptions }, }, @@ -121,6 +118,11 @@ interface DeepPartialArray extends Array> {} type DeepPartialObject = { [P in keyof T]+?: DeepPartial }; function createRawRequestMock(customization: DeepPartial = {}) { + const pathname = customization.url?.pathname || '/'; + const path = `${pathname}${customization.url?.search || ''}`; + const url = Object.assign({ pathname, path, href: path }, customization.url); + + // @ts-expect-error _core isn't supposed to be accessed - remove once we upgrade to hapi v18 return merge( {}, { @@ -129,17 +131,21 @@ function createRawRequestMock(customization: DeepPartial = {}) { isAuthenticated: true, }, headers: {}, - path: '/', + path, route: { settings: {} }, - url: { - href: '/', - }, + url, raw: { req: { - url: '/', + url: path, socket: {}, }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, customization ) as Request; diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 2440f2b1da0bd..d94bce12fb439 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -271,7 +271,7 @@ export class HttpServer { } this.registerOnPreRouting((request, response, toolkit) => { - const oldUrl = request.url.href!; + const oldUrl = request.url.pathname + request.url.search; const newURL = basePathService.remove(oldUrl); const shouldRedirect = newURL !== oldUrl; if (shouldRedirect) { diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 01817b29de8ac..37401a2c24ccd 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -124,7 +124,13 @@ describe('OnPreRouting', () => { const router = createRouter('/'); router.get({ path: '/login', validate: false }, (context, req, res) => { - return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + return res.ok({ + body: { + rewrittenUrl: req.rewrittenUrl + ? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}` + : undefined, + }, + }); }); registerOnPreRouting((req, res, t) => t.rewriteUrl('/login')); @@ -143,7 +149,13 @@ describe('OnPreRouting', () => { const router = createRouter('/'); router.get({ path: '/reroute-2', validate: false }, (context, req, res) => { - return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + return res.ok({ + body: { + rewrittenUrl: req.rewrittenUrl + ? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}` + : undefined, + }, + }); }); registerOnPreRouting((req, res, t) => t.rewriteUrl('/reroute-1')); @@ -163,7 +175,13 @@ describe('OnPreRouting', () => { const router = createRouter('/'); router.get({ path: '/login', validate: false }, (context, req, res) => { - return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + return res.ok({ + body: { + rewrittenUrl: req.rewrittenUrl + ? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}` + : undefined, + }, + }); }); registerOnPreRouting((req, res, t) => t.next()); diff --git a/src/core/server/http/lifecycle/on_pre_routing.ts b/src/core/server/http/lifecycle/on_pre_routing.ts index 92ae1f0b7bbdf..e553f113a7cf8 100644 --- a/src/core/server/http/lifecycle/on_pre_routing.ts +++ b/src/core/server/http/lifecycle/on_pre_routing.ts @@ -17,6 +17,7 @@ * under the License. */ +import { URL } from 'url'; import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi'; import { Logger } from '../../logging'; import { @@ -110,10 +111,30 @@ export function adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) { if (preRoutingResult.isRewriteUrl(result)) { const appState = request.app as KibanaRequestState; - appState.rewrittenUrl = appState.rewrittenUrl ?? request.url; + appState.rewrittenUrl = + // @ts-expect-error request._core isn't supposed to be accessed - remove once we upgrade to hapi v18 + appState.rewrittenUrl ?? new URL(request.url.href!, request._core.info.uri); const { url } = result; - request.setUrl(url); + + // TODO: Remove once we upgrade to Node.js 12! + // + // Warning: The following for-loop took 10 days to write, and is a hack + // to force V8 to make a copy of the string in memory. + // + // The reason why we need this is because of what appears to be a bug + // in V8 that caused some URL paths to not be routed correctly once + // `request.setUrl` was called with the path. + // + // The details can be seen in this discussion on Twitter: + // https://twitter.com/wa7son/status/1319992632366518277 + let urlCopy = ''; + for (let i = 0; i < url.length; i++) { + urlCopy += url[i]; + } + + request.setUrl(urlCopy); + // We should update raw request as well since it can be proxied to the old platform request.raw.req.url = url; return responseToolkit.continue; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 2d0e8d6c1a6ad..561bf742050c3 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Url } from 'url'; +import { URL } from 'url'; import uuid from 'uuid'; import { Request, RouteOptionsApp, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; @@ -45,7 +45,7 @@ export interface KibanaRouteOptions extends RouteOptionsApp { export interface KibanaRequestState extends ApplicationState { requestId: string; requestUuid: string; - rewrittenUrl?: Url; + rewrittenUrl?: URL; } /** @@ -163,7 +163,7 @@ export class KibanaRequest< */ public readonly uuid: string; /** a WHATWG URL standard object. */ - public readonly url: Url; + public readonly url: URL; /** matched route details */ public readonly route: RecursiveReadonly>; /** @@ -190,7 +190,7 @@ export class KibanaRequest< /** * URL rewritten in onPreRouting request interceptor. */ - public readonly rewrittenUrl?: Url; + public readonly rewrittenUrl?: URL; /** @internal */ protected readonly [requestSymbol]: Request; @@ -212,7 +212,8 @@ export class KibanaRequest< this.uuid = appState?.requestUuid ?? uuid.v4(); this.rewrittenUrl = appState?.rewrittenUrl; - this.url = request.url; + // @ts-expect-error request._core isn't supposed to be accessed - remove once we upgrade to hapi v18 + this.url = new URL(request.url.href!, request._core.info.uri); this.headers = deepFreeze({ ...request.headers }); this.isSystemRequest = request.headers['kbn-system-request'] === 'true' || @@ -304,8 +305,8 @@ export class KibanaRequest< if (authOptions === false) return false; throw new Error( `unexpected authentication options: ${JSON.stringify(authOptions)} for route: ${ - this.url.href - }` + this.url.pathname + }${this.url.search}` ); } } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index d9dc46d2cad99..914b5fbdb5196 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -162,7 +162,7 @@ import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; -import { Url } from 'url'; +import { URL } from 'url'; // @public export interface AppCategory { @@ -1007,11 +1007,11 @@ export class KibanaRequest>; // (undocumented) readonly socket: IKibanaSocket; - readonly url: Url; + readonly url: URL; readonly uuid: string; } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 18cbd9f9c5fad..c8e2684651598 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -145,6 +145,12 @@ test('executes the task by calling the executor with proper parameters', async ( url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, }); }); @@ -271,6 +277,12 @@ test('uses API key when provided', async () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, }); }); @@ -310,6 +322,12 @@ test(`doesn't use API key when not provided`, async () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, }); }); diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index aeeeb4ed7d520..93ae5a2c807f9 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -102,6 +102,12 @@ export class TaskRunnerFactory { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; let executorResult: ActionTypeExecutorResult; diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 55c2f3ddd18a4..4e457cdb12bd3 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -60,6 +60,12 @@ const fakeRequest = ({ url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectsClient, } as unknown) as Request; diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index b13a1c62f6602..ece7d31d2f7fd 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -149,6 +149,12 @@ describe('Alerting Plugin', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: jest.fn(), } as unknown) as KibanaRequest; await startContract.getAlertsClientWithRequest(fakeRequest); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 8e345d6ff66a8..17d5fcd31b745 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -364,6 +364,12 @@ describe('Task Runner', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` @@ -662,6 +668,12 @@ describe('Task Runner', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }); }); @@ -694,6 +706,12 @@ describe('Task Runner', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }); }); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 954c5675df89c..76125da20d552 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -101,6 +101,12 @@ export class TaskRunner { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts index 67fc1a98ad4d1..4b1e4b34da86a 100644 --- a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts +++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse as parseUrl } from 'url'; import { OnPostAuthHandler, OnPostAuthToolkit, @@ -45,7 +46,7 @@ describe('DashboardOnlyModeRequestInterceptor', () => { test('should not redirects for not app/* requests', async () => { const request = ({ url: { - path: 'api/test', + pathname: 'api/test', }, } as unknown) as KibanaRequest; @@ -57,7 +58,7 @@ describe('DashboardOnlyModeRequestInterceptor', () => { test('should not redirects not authenticated users', async () => { const request = ({ url: { - path: '/app/home', + pathname: '/app/home', }, } as unknown) as KibanaRequest; @@ -70,10 +71,9 @@ describe('DashboardOnlyModeRequestInterceptor', () => { function testRedirectToDashboardModeApp(url: string) { describe(`requests to url:"${url}"`, () => { test('redirects to the dashboard_mode app instead', async () => { + const { pathname, search, hash } = parseUrl(url); const request = ({ - url: { - path: url, - }, + url: { pathname, search, hash }, credentials: { roles: [DASHBOARD_ONLY_MODE_ROLE], }, diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts index 4378c818f087c..9978d18142ff5 100644 --- a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts +++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts @@ -22,7 +22,7 @@ export const setupDashboardModeRequestInterceptor = ({ getUiSettingsClient, }: DashboardModeRequestInterceptorDependencies) => (async (request, response, toolkit) => { - const path = request.url.path || ''; + const path = request.url.pathname; const isAppRequest = path.startsWith('/app/'); if (!isAppRequest) { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 2bddc9f1c80bd..5bd15ce411002 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -21,7 +21,6 @@ describe('callEnterpriseSearchConfigAPI', () => { accessCheckTimeoutWarning: 100, }; const mockRequest = { - url: { path: '/app/kibana' }, headers: { authorization: '==someAuth' }, }; const mockDependencies = { diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index d6793be425585..d9846428b9488 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -322,6 +322,12 @@ function FakeRequest(): KibanaRequest { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectGetter, } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index 0a5b169e87d4d..db6f4a1ad0f27 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -56,6 +56,12 @@ function fakeRequest(): KibanaRequest { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectsClient, } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts index 6a02d54c87514..076260ab2fe53 100644 --- a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts @@ -93,6 +93,12 @@ function fakeRequest(): KibanaRequest { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectsClient, } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts index fe06de765bbff..1ad710ba70e29 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts @@ -23,6 +23,12 @@ const fakeRequest = ({ url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; export async function agentPolicyUpdateEventHandler( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts index 994ecc64c82a7..b9ef36ecaae54 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts @@ -23,6 +23,12 @@ function getInternalUserSOClient() { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; return appContextService.getInternalUserSOClient(fakeRequest); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index aa48d8fe18e9f..c0e8540004930 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -58,6 +58,12 @@ function getInternalUserSOClient() { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; return appContextService.getInternalUserSOClient(fakeRequest); diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index e0f1b01dafa13..48aed19ea9050 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -456,16 +456,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat }, mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { id: string; force?: boolean | undefined } = { + const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics({ id: request.params.analyticsId, - }; - // @ts-expect-error TODO: update types - if (request.url?.query?.force !== undefined) { - // @ts-expect-error TODO: update types - options.force = request.url.query.force; - } - - const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics(options); + force: request.query.force, + }); return response.ok({ body, }); diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index abd86d51fb6b6..d62adc62bc9aa 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -210,11 +210,18 @@ export class ReportingCore { } public getFakeRequest(baseRequest: object, spaceId: string | undefined, logger = this.logger) { + // @ts-expect-error _core isn't supposed to be accessed - remove once we upgrade to hapi v18 const fakeRequest = KibanaRequest.from({ path: '/', route: { settings: {} }, url: { href: '/' }, raw: { req: { url: '/' } }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, ...baseRequest, } as Hapi.Request); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index cee8a88000e29..cce002a0e6935 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -27,7 +27,7 @@ const getMockContext = () => const getMockRequest = () => ({ - url: { port: '5601', query: '', path: '/foo' }, + url: { port: '5601', search: '', pathname: '/foo' }, route: { path: '/foo', options: {} }, } as KibanaRequest); diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts index 1978795f82a24..f153b9efb9d43 100644 --- a/x-pack/plugins/security/server/audit/audit_events.test.ts +++ b/x-pack/plugins/security/server/audit/audit_events.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { URL } from 'url'; import { EventOutcome, SavedObjectAction, @@ -192,11 +193,11 @@ describe('#httpRequestEvent', () => { }, "message": "User is requesting [/path] endpoint", "url": Object { - "domain": undefined, + "domain": "localhost", "path": "/path", "port": undefined, "query": undefined, - "scheme": undefined, + "scheme": "http:", }, } `); @@ -211,12 +212,7 @@ describe('#httpRequestEvent', () => { kibanaRequestState: { requestId: '123', requestUuid: '123e4567-e89b-12d3-a456-426614174000', - rewrittenUrl: { - path: '/original/path', - pathname: '/original/path', - query: 'query=param', - search: '?query=param', - }, + rewrittenUrl: new URL('http://localhost/original/path?query=param'), }, }), }) @@ -234,11 +230,11 @@ describe('#httpRequestEvent', () => { }, "message": "User is requesting [/original/path] endpoint", "url": Object { - "domain": undefined, + "domain": "localhost", "path": "/original/path", "port": undefined, "query": "query=param", - "scheme": undefined, + "scheme": "http:", }, } `); diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 1ff9da3a95ff4..d91c18bf82e02 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -105,10 +105,10 @@ export interface HttpRequestParams { } export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent { - const { pathname, search } = request.rewrittenUrl ?? request.url; + const url = request.rewrittenUrl ?? request.url; return { - message: `User is requesting [${pathname}] endpoint`, + message: `User is requesting [${url.pathname}] endpoint`, event: { action: 'http_request', category: EventCategory.WEB, @@ -120,11 +120,11 @@ export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent { }, }, url: { - domain: request.url.hostname, - path: pathname, - port: request.url.port ? parseInt(request.url.port, 10) : undefined, - query: search?.slice(1) || undefined, - scheme: request.url.protocol, + domain: url.hostname, + path: url.pathname, + port: url.port ? parseInt(url.port, 10) : undefined, + query: url.search ? url.search.slice(1) : undefined, + scheme: url.protocol, }, }; } diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 3b587182c491c..80aeb4f8b2959 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -333,7 +333,7 @@ export class Authenticator { this.logger.debug('Redirecting request to Login Selector.'); return AuthenticationResult.redirectTo( `${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}` ); } @@ -728,7 +728,7 @@ export class Authenticator { preAccessRedirectURL = `${preAccessRedirectURL}?next=${encodeURIComponent( authenticationResult.redirectURL || redirectURL || - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}`; } else if (redirectURL && !authenticationResult.redirectURL) { preAccessRedirectURL = redirectURL; diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index 2481844abb389..87002ebed5672 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -101,13 +101,13 @@ describe('BasicAuthenticationProvider', () => { await expect( provider.authenticate( httpServerMock.createKibanaRequest({ - path: '/s/foo/some-path # that needs to be encoded', + path: '/s/foo/some path that needs to be encoded', }), null ) ).resolves.toEqual( AuthenticationResult.redirectTo( - '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome%2520path%2520that%2520needs%2520to%2520be%2520encoded' ) ); }); diff --git a/x-pack/plugins/security/server/authentication/providers/basic.ts b/x-pack/plugins/security/server/authentication/providers/basic.ts index 35ab2d242659a..28b671346ee7f 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.ts @@ -90,7 +90,9 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -106,7 +108,9 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Redirecting request to Login page.'); const basePath = this.options.basePath.get(request); return AuthenticationResult.redirectTo( - `${basePath}/login?next=${encodeURIComponent(`${basePath}${request.url.path}`)}` + `${basePath}/login?next=${encodeURIComponent( + `${basePath}${request.url.pathname}${request.url.search}` + )}` ); } @@ -119,7 +123,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. diff --git a/x-pack/plugins/security/server/authentication/providers/http.ts b/x-pack/plugins/security/server/authentication/providers/http.ts index 3e33a52cbbc6b..933685d68978f 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.ts @@ -56,7 +56,9 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ public async authenticate(request: KibanaRequest) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request); if (authorizationHeader == null) { @@ -72,12 +74,12 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { try { const user = await this.getUser(request); this.logger.debug( - `Request to ${request.url.path} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.` + `Request to ${request.url.pathname}${request.url.search} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.` ); return AuthenticationResult.succeeded(user); } catch (err) { this.logger.debug( - `Failed to authenticate request to ${request.url.path} via authorization header with "${authorizationHeader.scheme}" scheme: ${err.message}` + `Failed to authenticate request to ${request.url.pathname}${request.url.search} via authorization header with "${authorizationHeader.scheme}" scheme: ${err.message}` ); return AuthenticationResult.failed(err); } diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index 5b593851cc2f2..d7de71f4da9ed 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -65,7 +65,9 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request); if (authorizationHeader && authorizationHeader.scheme.toLowerCase() !== 'negotiate') { @@ -100,7 +102,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index 75c909cdcd94b..9570c59f8ea1d 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -166,7 +166,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -418,7 +420,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. @@ -477,7 +479,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { `${ this.options.basePath.serverBasePath }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( this.options.name )}`, diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index f3cc21500df26..6dcb448e08150 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -61,7 +61,9 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -105,7 +107,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index cf6772332b8b6..59a1782c1f1fd 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -193,7 +193,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -232,7 +234,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Normally when there is no active session in Kibana, `logout` method shouldn't do anything // and user will eventually be redirected to the home page to log in. But when SAML SLO is @@ -631,7 +633,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { `${ this.options.basePath.serverBasePath }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( this.options.name )}`, diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 0264edf4fc082..ffb1c89b24e47 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -173,13 +173,13 @@ describe('TokenAuthenticationProvider', () => { await expect( provider.authenticate( httpServerMock.createKibanaRequest({ - path: '/s/foo/some-path # that needs to be encoded', + path: '/s/foo/some path that needs to be encoded', }), null ) ).resolves.toEqual( AuthenticationResult.redirectTo( - '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome%2520path%2520that%2520needs%2520to%2520be%2520encoded' ) ); }); diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 869fd69173e2e..7dace488bc95a 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -92,7 +92,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -126,7 +128,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. @@ -241,7 +243,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ private getLoginPageURL(request: KibanaRequest) { - const nextURL = encodeURIComponent(`${this.options.basePath.get(request)}${request.url.path}`); + const nextURL = encodeURIComponent( + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` + ); return `${this.options.basePath.get(request)}/login?next=${nextURL}`; } } diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts index 813ed8d064d94..9cf090ab271ae 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -33,11 +33,13 @@ export function initAPIAuthorization( // we've actually authorized the request if (checkPrivilegesResponse.hasAllRequested) { - logger.debug(`User authorized for "${request.url.path}"`); + logger.debug(`User authorized for "${request.url.pathname}${request.url.search}"`); return toolkit.next(); } - logger.warn(`User not authorized for "${request.url.path}": responding with 403`); + logger.warn( + `User not authorized for "${request.url.pathname}${request.url.search}": responding with 403` + ); return response.forbidden(); }); } diff --git a/x-pack/plugins/security/server/authorization/authorization_service.tsx b/x-pack/plugins/security/server/authorization/authorization_service.tsx index 9547295af4dfb..a45bca90d8b56 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.tsx +++ b/x-pack/plugins/security/server/authorization/authorization_service.tsx @@ -168,7 +168,7 @@ export class AuthorizationService { http.registerOnPreResponse((request, preResponse, toolkit) => { if (preResponse.statusCode === 403 && canRedirectRequest(request)) { const basePath = http.basePath.get(request); - const next = `${basePath}${request.url.path}`; + const next = `${basePath}${request.url.pathname}${request.url.search}`; const regularBundlePath = `${basePath}/${buildNumber}/bundles`; const logoutUrl = http.basePath.prepend( diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 5d8a7ae7bdfea..7eaa619b330e0 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -135,7 +135,7 @@ export function defineOIDCRoutes({ loginAttempt = { type: OIDCLogin.LoginWithAuthorizationCodeFlow, // We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway. - authenticationResponseURI: request.url.path!, + authenticationResponseURI: request.url.pathname + request.url.search, }; } else if (request.query.iss) { logger.warn( diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index fee3adbb19f97..b90a44be7aade 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -100,7 +100,7 @@ describe('Login view routes', () => { auth: { isAuthenticated: true }, }); (request as any).url = new URL( - `${request.url.path}${request.url.search}`, + `${request.url.pathname}${request.url.search}`, 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: true } as any); @@ -114,7 +114,7 @@ describe('Login view routes', () => { // Redirect if `showLogin` is `false` even if user is not authenticated. request = httpServerMock.createKibanaRequest({ query, auth: { isAuthenticated: false } }); (request as any).url = new URL( - `${request.url.path}${request.url.search}`, + `${request.url.pathname}${request.url.search}`, 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: false } as any); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index e3a724e153688..1aa2011a15b35 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -28,7 +28,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ http.registerOnPostAuth(async (request, response, toolkit) => { const serverBasePath = http.basePath.serverBasePath; - const path = request.url.pathname!; + const path = request.url.pathname; const spaceId = spacesService.getSpaceId(request); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 6408803c2114b..a3335b1e075f2 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -9,8 +9,6 @@ import { LifecycleResponseFactory, CoreSetup, } from 'src/core/server'; -import { format } from 'url'; -import { modifyUrl } from '../utils/url'; import { getSpaceIdFromPath } from '../../../common'; export interface OnRequestInterceptorDeps { @@ -34,16 +32,9 @@ export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDep http.basePath.set(request, reqBasePath); - const newLocation = (path && path.substr(reqBasePath.length)) || '/'; + const newPathname = path.substr(reqBasePath.length) || '/'; - const newUrl = modifyUrl(format(request.url), (parts) => { - return { - ...parts, - pathname: newLocation, - }; - }); - - return toolkit.rewriteUrl(newUrl); + return toolkit.rewriteUrl(`${newPathname}${request.url.search}`); } return toolkit.next(); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index b48bf971d0c1b..d1e1d81134940 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -58,7 +58,7 @@ const createService = async (serverBasePath: string = '') => { serverBasePath, } as HttpServiceSetup['basePath']; httpSetup.basePath.get = jest.fn().mockImplementation((request: KibanaRequest) => { - const { spaceId } = getSpaceIdFromPath(request.url.path); + const { spaceId } = getSpaceIdFromPath(request.url.pathname); if (spaceId !== DEFAULT_SPACE_ID) { return `/s/${spaceId}`; @@ -83,7 +83,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/app/kibana' }, + url: { pathname: '/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.getSpaceId(request)).toEqual(DEFAULT_SPACE_ID); @@ -93,7 +93,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/s/foo/app/kibana' }, + url: { pathname: '/s/foo/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.getSpaceId(request)).toEqual('foo'); @@ -140,7 +140,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/app/kibana' }, + url: { pathname: '/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(true); @@ -150,7 +150,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/s/foo/app/kibana' }, + url: { pathname: '/s/foo/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(false); From 2d49dea005847cac6855deb7c41ab18241af02e6 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 29 Oct 2020 08:53:12 -0500 Subject: [PATCH 27/73] [deb/rpm] set logging.dest (#74896) Co-authored-by: Elastic Machine --- .../tasks/os_packages/package_scripts/post_install.sh | 9 ++++++--- src/dev/build/tasks/os_packages/run_fpm.ts | 2 ++ .../systemd/etc/systemd/system/kibana.service | 2 +- .../os_packages/service_templates/sysv/etc/init.d/kibana | 9 +++------ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh index 1c679bdb40b59..939226b565f79 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh @@ -7,14 +7,17 @@ set_chmod() { chmod -f 660 ${KBN_PATH_CONF}/kibana.yml || true chmod -f 2750 <%= dataDir %> || true chmod -f 2750 ${KBN_PATH_CONF} || true + chmod -f 2750 <%= logDir %> || true } set_chown() { + chown <%= user %>:<%= group %> <%= logDir %> chown -R <%= user %>:<%= group %> <%= dataDir %> chown -R root:<%= group %> ${KBN_PATH_CONF} } -set_access() { +setup() { + [ ! -d "<%= logDir %>" ] && mkdir "<%= logDir %>" set_chmod set_chown } @@ -35,7 +38,7 @@ case $1 in IS_UPGRADE=true fi - set_access + setup ;; abort-deconfigure|abort-upgrade|abort-remove) ;; @@ -55,7 +58,7 @@ case $1 in IS_UPGRADE=true fi - set_access + setup ;; *) diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index b5169ec3d43b6..b8289f1da194f 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -109,6 +109,8 @@ export async function runFpm( `pluginsDir=/usr/share/kibana/plugins`, '--template-value', `dataDir=/var/lib/kibana`, + '--template-value', + `logDir=/var/log/kibana`, // config and data directories are copied to /usr/share and /var/lib // below, so exclude them from the main package source located in diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service index df33b82f1f967..05724db8799f3 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service @@ -15,7 +15,7 @@ Environment=KBN_PATH_CONF=/etc/kibana EnvironmentFile=-/etc/default/kibana EnvironmentFile=-/etc/sysconfig/kibana -ExecStart=/usr/share/kibana/bin/kibana +ExecStart=/usr/share/kibana/bin/kibana --logging.dest="/var/log/kibana/kibana.log" Restart=on-failure RestartSec=3 diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index c13676ef031b0..eedd4898ce6c3 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -35,6 +35,7 @@ fi name=kibana program=/usr/share/kibana/bin/kibana +args="--logging.dest=/var/log/kibana/kibana.log" pidfile="/var/run/kibana/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name @@ -55,10 +56,6 @@ emit() { } start() { - [ ! -d "/var/log/kibana/" ] && mkdir "/var/log/kibana/" - chown "$user":"$group" "/var/log/kibana/" - chmod 2750 "/var/log/kibana/" - [ ! -d "/var/run/kibana/" ] && mkdir "/var/run/kibana/" chown "$user":"$group" "/var/run/kibana/" chmod 755 "/var/run/kibana/" @@ -66,8 +63,8 @@ start() { chroot --userspec "$user":"$group" "$chroot" sh -c " cd \"$chdir\" - exec \"$program\" - " >> /var/log/kibana/kibana.stdout 2>> /var/log/kibana/kibana.stderr & + exec \"$program $args\" + " >> /var/log/kibana/kibana.log 2>&1 & # Generate the pidfile from here. If we instead made the forked process # generate it there will be a race condition between the pidfile writing From 40fc944f30b121a6986df50dd30ef77ad90aad41 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 29 Oct 2020 10:32:13 -0400 Subject: [PATCH 28/73] add experimental when getting packages, add integration tests for side effects like recreation of index patterns (#81940) --- .../server/services/epm/packages/get.ts | 2 +- .../apis/epm/install_remove_multiple.ts | 106 ++++++++++++++++++ .../data_stream/test_logs/fields/ecs.yml | 3 + .../data_stream/test_logs/fields/fields.yml | 16 +++ .../0.1.0/data_stream/test_logs/manifest.yml | 9 ++ .../data_stream/test_metrics/fields/ecs.yml | 3 + .../test_metrics/fields/fields.yml | 16 +++ .../data_stream/test_metrics/manifest.yml | 3 + .../experimental/0.1.0/docs/README.md | 3 + .../0.1.0/img/logo_overrides_64_color.svg | 7 ++ .../experimental/0.1.0/manifest.yml | 20 ++++ .../data_stream/test_logs/fields/ecs.yml | 3 + .../data_stream/test_logs/fields/fields.yml | 16 +++ .../0.1.0/data_stream/test_logs/manifest.yml | 9 ++ .../data_stream/test_metrics/fields/ecs.yml | 3 + .../test_metrics/fields/fields.yml | 16 +++ .../data_stream/test_metrics/manifest.yml | 3 + .../experimental2/0.1.0/docs/README.md | 3 + .../0.1.0/img/logo_overrides_64_color.svg | 7 ++ .../experimental2/0.1.0/manifest.yml | 20 ++++ 20 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_multiple.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/img/logo_overrides_64_color.svg create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/img/logo_overrides_64_color.svg create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/manifest.yml diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index 2cf94e9c16079..cd0dcba7b97b2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -86,7 +86,7 @@ export async function getPackageKeysByStatus( savedObjectsClient: SavedObjectsClientContract, status: InstallationStatus ) { - const allPackages = await getPackages({ savedObjectsClient }); + const allPackages = await getPackages({ savedObjectsClient, experimental: true }); return allPackages.reduce>((acc, pkg) => { if (pkg.status === status) { if (pkg.status === InstallationStatus.installed) { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_multiple.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_multiple.ts new file mode 100644 index 0000000000000..82072f59a482b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_multiple.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); + const pkgName = 'all_assets'; + const pkgVersion = '0.1.0'; + const pkgKey = `${pkgName}-${pkgVersion}`; + const experimentalPkgName = 'experimental'; + const experimentalPkgKey = `${experimentalPkgName}-${pkgVersion}`; + const experimental2PkgName = 'experimental2'; + const experimental2PkgKey = `${experimental2PkgName}-${pkgVersion}`; + + const uninstallPackage = async (pkg: string) => { + await supertest.delete(`/api/fleet/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + }; + const installPackage = async (pkg: string) => { + await supertest + .post(`/api/fleet/epm/packages/${pkg}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + const installPackages = async (pkgs: string[]) => { + const installingPackagesPromise = pkgs.map((pkg) => installPackage(pkg)); + return Promise.all(installingPackagesPromise); + }; + const uninstallPackages = async (pkgs: string[]) => { + const uninstallingPackagesPromise = pkgs.map((pkg) => uninstallPackage(pkg)); + return Promise.all(uninstallingPackagesPromise); + }; + const expectPkgFieldToExist = async ( + fields: any[], + fieldName: string, + exists: boolean = true + ) => { + const fieldExists = fields.find((field: { name: string }) => field.name === fieldName); + if (exists) { + expect(fieldExists).not.to.be(undefined); + } else { + expect(fieldExists).to.be(undefined); + } + }; + describe('installs and uninstalls multiple packages side effects', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + if (!server.enabled) return; + await installPackages([pkgKey, experimentalPkgKey, experimental2PkgKey]); + }); + after(async () => { + if (!server.enabled) return; + await uninstallPackages([pkgKey, experimentalPkgKey, experimental2PkgKey]); + }); + it('should create index patterns from all installed packages, experimental or beta', async () => { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + + const fieldsLogs = JSON.parse(resIndexPatternLogs.attributes.fields); + + expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); + expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); + expectPkgFieldToExist(fieldsLogs, 'logs_experimental2_name'); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name'); + }); + it('should correctly recreate index patterns when a package is uninstalled', async () => { + await uninstallPackage(experimental2PkgKey); + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + const fields = JSON.parse(resIndexPatternLogs.attributes.fields); + expectPkgFieldToExist(fields, 'logs_test_name'); + expectPkgFieldToExist(fields, 'logs_experimental_name'); + expectPkgFieldToExist(fields, 'logs_experimental2_name', false); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + + expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name', false); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/ecs.yml new file mode 100644 index 0000000000000..4c18291b7c5ad --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: logs_experimental_name + title: logs_experimental_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/ecs.yml new file mode 100644 index 0000000000000..3cd2db230f437 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: metrics_experimental_name + title: metrics_experimental_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/manifest.yml new file mode 100644 index 0000000000000..6bc20442bd432 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/docs/README.md new file mode 100644 index 0000000000000..8e524c4c71b5f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing side effects when installing and removing multiple packages diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml new file mode 100644 index 0000000000000..9c83569a69cbe --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: experimental +title: experimental integration +description: This is a test package for testing experimental packages +version: 0.1.0 +categories: [] +release: experimental +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/ecs.yml new file mode 100644 index 0000000000000..dad07fa9637af --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: logs_experimental2_name + title: logs_experimental2_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/ecs.yml new file mode 100644 index 0000000000000..0b6a2efaacd33 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: metrics_experimental2_name + title: metrics_experimental2_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/manifest.yml new file mode 100644 index 0000000000000..6bc20442bd432 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/docs/README.md new file mode 100644 index 0000000000000..8e524c4c71b5f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing side effects when installing and removing multiple packages diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/manifest.yml new file mode 100644 index 0000000000000..766835dbde037 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: experimental2 +title: experimental integration +description: This is a test package for testing experimental packages +version: 0.1.0 +categories: [] +release: experimental +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' From 6bff52c66e4e9482b712abacb55a40eb722cc075 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian Date: Thu, 29 Oct 2020 09:46:23 -0500 Subject: [PATCH 29/73] [Uptime] Fix broken overview page when no summary data present (#81952) Fixes https://github.com/elastic/kibana/issues/81950 by not assuming the summary is present in a bucket with partial check info --- .../search/refine_potential_matches.ts | 6 +++++ .../uptime/rest/monitor_states_generated.ts | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 98db43c5b2623..a864bfa591424 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -38,6 +38,12 @@ export const fullyMatchingIds = (queryResult: any, statusFilter?: string): Monit for (const locBucket of monBucket.location.buckets) { const latest = locBucket.summaries.latest.hits.hits[0]; + // It is possible for no latest summary to exist in this bucket if only partial + // non-summary docs exist + if (!latest) { + continue; + } + const latestStillMatching = locBucket.latest_matching.top.hits.hits[0]; // If the most recent document still matches the most recent document matching the current filters // we can include this in the result diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts index 3e06373042d59..69571099a2642 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -24,6 +24,30 @@ export default function ({ getService }: FtrProviderContext) { before('load heartbeat data', () => getService('esArchiver').load('uptime/blank')); after('unload heartbeat index', () => getService('esArchiver').unload('uptime/blank')); + // In this case we don't actually have any monitors to display + // but the query should still return successfully. This has + // caused bugs in the past because a bucket of monitor data + // was available and the query code assumed at least one + // event would be a summary for each monitor. + // See https://github.com/elastic/kibana/issues/81950 + describe('checks with no summaries', async () => { + const testMonitorId = 'scope-test-id'; + before(async () => { + const es = getService('legacyEs'); + dateRangeStart = new Date().toISOString(); + await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', (d) => { + delete d.summary; + return d; + }); + }); + + it('should return no monitors and have no errors', async () => { + const url = getBaseUrl(dateRangeStart, new Date().toISOString()); + const apiResponse = await supertest.get(url); + expect(apiResponse.status).to.equal(200); + }); + }); + describe('query document scoping with mismatched check statuses', async () => { let checks: any[] = []; let nonSummaryIp: string | null = null; From b5e3e18ea4b478f5a7dfb4b3a8bfa66ebed07fad Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 29 Oct 2020 10:48:49 -0400 Subject: [PATCH 30/73] [Lens] Stop using multi-level metrics in Lens pie charts (#81523) * [Lens] Stop using multi-level metrics in Lens * Fix linting * Simplify even more --- .../indexpattern.test.ts | 58 ++++++++++- .../indexpattern_datasource/to_expression.ts | 24 +---- .../pie_visualization/render_function.tsx | 41 +++----- .../pie_visualization/render_helpers.test.ts | 95 ++++++------------- .../pie_visualization/render_helpers.ts | 13 +-- .../lens/public/pie_visualization/types.ts | 6 -- 6 files changed, 99 insertions(+), 138 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 900cd02622aaf..77dc6f97fb236 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -319,10 +319,10 @@ describe('IndexPattern Data Source', () => { "1", ], "metricsAtAllLevels": Array [ - true, + false, ], "partialRows": Array [ - true, + false, ], "timeFields": Array [ "timestamp", @@ -334,7 +334,7 @@ describe('IndexPattern Data Source', () => { Object { "arguments": Object { "idMap": Array [ - "{\\"col--1-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-2-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", + "{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", ], }, "function": "lens_rename_columns", @@ -392,6 +392,58 @@ describe('IndexPattern Data Source', () => { expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); + it('should rename the output from esaggs when using flat query', () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['bucket1', 'bucket2', 'metric'], + columns: { + metric: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + bucket1: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + }, + }, + bucket2: { + label: 'Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'geo.src', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 10, + }, + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.metricsAtAllLevels).toEqual([false]); + expect(JSON.parse(ast.chain[1].arguments.idMap[0] as string)).toEqual({ + 'col-0-bucket1': expect.any(Object), + 'col-1-bucket2': expect.any(Object), + 'col-2-metric': expect.any(Object), + }); + }); + it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index e2c4323b56c2a..ea7aa62054e5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -29,34 +29,16 @@ function getExpressionForLayer( } const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const); - const bucketsCount = columnEntries.filter(([, entry]) => entry.isBucketed).length; - const metricsCount = columnEntries.length - bucketsCount; if (columnEntries.length) { const aggs = columnEntries.map(([colId, col]) => { return getEsAggsConfig(col, colId); }); - /** - * Because we are turning on metrics at all levels, the sequence generation - * logic here is more complicated. Examples follow: - * - * Example 1: [Count] - * Output: [`col-0-count`] - * - * Example 2: [Terms, Terms, Count] - * Output: [`col-0-terms0`, `col-2-terms1`, `col-3-count`] - * - * Example 3: [Terms, Terms, Count, Max] - * Output: [`col-0-terms0`, `col-3-terms1`, `col-4-count`, `col-5-max`] - */ const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => { - const newIndex = column.isBucketed - ? index * (metricsCount + 1) // Buckets are spaced apart by N + 1 - : (index ? index + 1 : 0) - bucketsCount + (bucketsCount - 1) * (metricsCount + 1); return { ...currentIdMap, - [`col-${columnEntries.length === 1 ? 0 : newIndex}-${colId}`]: { + [`col-${columnEntries.length === 1 ? 0 : index}-${colId}`]: { ...column, id: colId, }, @@ -122,8 +104,8 @@ function getExpressionForLayer( function: 'esaggs', arguments: { index: [indexPattern.id], - metricsAtAllLevels: [true], - partialRows: [true], + metricsAtAllLevels: [false], + partialRows: [false], includeFormatHints: [true], timeFields: allDateHistogramFields, aggConfigs: [JSON.stringify(aggs)], diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index cb2458a76967c..d4c85ce9b8843 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -27,8 +27,8 @@ import { import { FormatFactory, LensFilterEvent } from '../types'; import { VisualizationContainer } from '../visualization_container'; import { CHART_NAMES, DEFAULT_PERCENT_DECIMALS } from './constants'; -import { ColumnGroups, PieExpressionProps } from './types'; -import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; +import { PieExpressionProps } from './types'; +import { getSliceValue, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; import { desanitizeFilterContext } from '../utils'; @@ -72,21 +72,6 @@ export function PieComponent( }); } - // The datatable for pie charts should include subtotals, like this: - // [bucket, subtotal, bucket, count] - // But the user only configured [bucket, bucket, count] - const columnGroups: ColumnGroups = []; - firstTable.columns.forEach((col) => { - if (groups.includes(col.id)) { - columnGroups.push({ - col, - metrics: [], - }); - } else if (columnGroups.length > 0) { - columnGroups[columnGroups.length - 1].metrics.push(col); - } - }); - const fillLabel: Partial = { textInvertible: false, valueFont: { @@ -100,7 +85,9 @@ export function PieComponent( fillLabel.valueFormatter = () => ''; } - const layers: PartitionLayer[] = columnGroups.map(({ col }, layerIndex) => { + const bucketColumns = firstTable.columns.filter((col) => groups.includes(col.id)); + + const layers: PartitionLayer[] = bucketColumns.map((col, layerIndex) => { return { groupByRollup: (d: Datum) => d[col.id] ?? EMPTY_SLICE, showAccessor: (d: Datum) => d !== EMPTY_SLICE, @@ -116,7 +103,7 @@ export function PieComponent( fillLabel: isDarkMode && shape === 'treemap' && - layerIndex < columnGroups.length - 1 && + layerIndex < bucketColumns.length - 1 && categoryDisplay !== 'hide' ? { ...fillLabel, textColor: euiDarkVars.euiTextColor } : fillLabel, @@ -136,10 +123,10 @@ export function PieComponent( if (shape === 'treemap') { // Only highlight the innermost color of the treemap, as it accurately represents area - return layerIndex < columnGroups.length - 1 ? 'rgba(0,0,0,0)' : outputColor; + return layerIndex < bucketColumns.length - 1 ? 'rgba(0,0,0,0)' : outputColor; } - const lighten = (d.depth - 1) / (columnGroups.length * 2); + const lighten = (d.depth - 1) / (bucketColumns.length * 2); return color(outputColor, 'hsl').lighten(lighten).hex(); }, }, @@ -198,8 +185,6 @@ export function PieComponent( setState({ isReady: true }); }, []); - const reverseGroups = [...columnGroups].reverse(); - const hasNegative = firstTable.rows.some((row) => { const value = row[metricColumn.id]; return typeof value === 'number' && value < 0; @@ -243,16 +228,12 @@ export function PieComponent( showLegend={ !hideLabels && (legendDisplay === 'show' || - (legendDisplay === 'default' && columnGroups.length > 1 && shape !== 'treemap')) + (legendDisplay === 'default' && bucketColumns.length > 1 && shape !== 'treemap')) } legendPosition={legendPosition || Position.Right} legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */} onElementClick={(args) => { - const context = getFilterContext( - args[0][0] as LayerValue[], - columnGroups.map(({ col }) => col.id), - firstTable - ); + const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable); onClickValue(desanitizeFilterContext(context)); }} @@ -262,7 +243,7 @@ export function PieComponent( getSliceValueWithFallback(d, reverseGroups, metricColumn)} + valueAccessor={(d: Datum) => getSliceValue(d, metricColumn)} percentFormatter={(d: number) => percentFormatter.convert(d / 100)} valueGetter={hideLabels || numberDisplay === 'value' ? undefined : 'percent'} valueFormatter={(d: number) => (hideLabels ? '' : formatters[metricColumn.id].convert(d))} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index d9ccda2a99ab2..22c63cd67281b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -5,86 +5,47 @@ */ import { Datatable } from 'src/plugins/expressions/public'; -import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; -import { ColumnGroups } from './types'; +import { getSliceValue, getFilterContext } from './render_helpers'; describe('render helpers', () => { - describe('#getSliceValueWithFallback', () => { - describe('without fallback', () => { - const columnGroups: ColumnGroups = [ - { col: { id: 'a', name: 'A', meta: { type: 'string' } }, metrics: [] }, - { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, - ]; - - it('returns the metric when positive number', () => { - expect( - getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 5 }, columnGroups, { + describe('#getSliceValue', () => { + it('returns the metric when positive number', () => { + expect( + getSliceValue( + { a: 'Cat', b: 'Home', c: 5 }, + { id: 'c', name: 'C', meta: { type: 'number' }, - }) - ).toEqual(5); - }); + } + ) + ).toEqual(5); + }); - it('returns the metric when negative number', () => { - expect( - getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: -100 }, columnGroups, { + it('returns the metric when negative number', () => { + expect( + getSliceValue( + { a: 'Cat', b: 'Home', c: -100 }, + { id: 'c', name: 'C', meta: { type: 'number' }, - }) - ).toEqual(-100); - }); + } + ) + ).toEqual(-100); + }); - it('returns epsilon when metric is 0 without fallback', () => { - expect( - getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 0 }, columnGroups, { + it('returns epsilon when metric is 0 without fallback', () => { + expect( + getSliceValue( + { a: 'Cat', b: 'Home', c: 0 }, + { id: 'c', name: 'C', meta: { type: 'number' }, - }) - ).toEqual(Number.EPSILON); - }); - }); - - describe('fallback behavior', () => { - const columnGroups: ColumnGroups = [ - { - col: { id: 'a', name: 'A', meta: { type: 'string' } }, - metrics: [{ id: 'a_subtotal', name: '', meta: { type: 'number' } }], - }, - { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, - ]; - - it('falls back to metric from previous column if available', () => { - expect( - getSliceValueWithFallback( - { a: 'Cat', a_subtotal: 5, b: 'Home', c: undefined }, - columnGroups, - { id: 'c', name: 'C', meta: { type: 'number' } } - ) - ).toEqual(5); - }); - - it('uses epsilon if fallback is 0', () => { - expect( - getSliceValueWithFallback( - { a: 'Cat', a_subtotal: 0, b: 'Home', c: undefined }, - columnGroups, - { id: 'c', name: 'C', meta: { type: 'number' } } - ) - ).toEqual(Number.EPSILON); - }); - - it('uses epsilon if fallback is missing', () => { - expect( - getSliceValueWithFallback( - { a: 'Cat', a_subtotal: undefined, b: 'Home', c: undefined }, - columnGroups, - { id: 'c', name: 'C', meta: { type: 'number' } } - ) - ).toEqual(Number.EPSILON); - }); + } + ) + ).toEqual(Number.EPSILON); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index 26b4f9ccda853..978afcca6a550 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -6,22 +6,13 @@ import { Datum, LayerValue } from '@elastic/charts'; import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; -import { ColumnGroups } from './types'; import { LensFilterEvent } from '../types'; -export function getSliceValueWithFallback( - d: Datum, - reverseGroups: ColumnGroups, - metricColumn: DatatableColumn -) { +export function getSliceValue(d: Datum, metricColumn: DatatableColumn) { if (typeof d[metricColumn.id] === 'number' && d[metricColumn.id] !== 0) { return d[metricColumn.id]; } - // Sometimes there is missing data for outer groups - // When there is missing data, we fall back to the next groups - // This creates a sunburst effect - const hasMetric = reverseGroups.find((group) => group.metrics.length && d[group.metrics[0].id]); - return hasMetric ? d[hasMetric.metrics[0].id] || Number.EPSILON : Number.EPSILON; + return Number.EPSILON; } export function getFilterContext( diff --git a/x-pack/plugins/lens/public/pie_visualization/types.ts b/x-pack/plugins/lens/public/pie_visualization/types.ts index 0596e54870a94..54bececa13c2a 100644 --- a/x-pack/plugins/lens/public/pie_visualization/types.ts +++ b/x-pack/plugins/lens/public/pie_visualization/types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DatatableColumn } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; export interface SharedLayerState { @@ -38,8 +37,3 @@ export interface PieExpressionProps { data: LensMultiTable; args: PieExpressionArgs; } - -export type ColumnGroups = Array<{ - col: DatatableColumn; - metrics: DatatableColumn[]; -}>; From 667ff6cd2cf54ace0b67490b2ea6174df3c737e2 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Thu, 29 Oct 2020 15:03:18 +0000 Subject: [PATCH 31/73] [User experience] Enhance page load duration metrics (#81915) * Enhance page load duration metrics and helper tooltips --- .../step_definitions/csm/csm_dashboard.ts | 2 +- .../step_definitions/csm/csm_filters.ts | 4 +- .../step_definitions/csm/percentile_select.ts | 2 +- .../csm/service_name_filter.ts | 2 +- .../app/RumDashboard/ClientMetrics/index.tsx | 44 ++++++++++++++-- .../app/RumDashboard/RumDashboard.tsx | 3 +- .../RumDashboard/UXMetrics/KeyUXMetrics.tsx | 51 ++++++++++++++++--- .../UXMetrics/__tests__/KeyUXMetrics.test.tsx | 28 +++++++--- .../RumDashboard/UXMetrics/translations.ts | 40 +++++++++++++++ .../app/RumDashboard/translations.ts | 21 ++++++++ .../__snapshots__/queries.test.ts.snap | 4 +- .../lib/rum_client/get_client_metrics.ts | 24 +++++---- 12 files changed, 191 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts index a8edf862ab256..d8540c3f3efd7 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts @@ -26,7 +26,7 @@ Given(`a user browses the APM UI application for RUM Data`, () => { }); Then(`should have correct client metrics`, () => { - const metrics = ['4 ms', '58 ms', '55']; + const metrics = ['80 ms', '4 ms', '76 ms', '55']; verifyClientMetrics(metrics, true); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts index 5c2109bb518c2..88287286c66c5 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts @@ -56,7 +56,9 @@ Then(/^it filters the client metrics "([^"]*)"$/, (filterName) => { cy.get('.euiStat__title-isLoading').should('not.be.visible'); const data = - filterName === 'os' ? ['5 ms', '64 ms', '8'] : ['4 ms', '55 ms', '28']; + filterName === 'os' + ? ['82 ms', '5 ms', '77 ms', '8'] + : ['75 ms', '4 ms', '71 ms', '28']; verifyClientMetrics(data, true); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts index 314254883b2fd..44802bbce6208 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts @@ -16,7 +16,7 @@ When('the user changes the selected percentile', () => { }); Then(`it displays client metric related to that percentile`, () => { - const metrics = ['14 ms', '131 ms', '55']; + const metrics = ['165 ms', '14 ms', '151 ms', '55']; verifyClientMetrics(metrics, false); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts index 20c6a3fb72aa9..609d0d18f5bc8 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts @@ -15,7 +15,7 @@ When('the user changes the selected service name', () => { }); Then(`it displays relevant client metrics`, () => { - const metrics = ['4 ms', '58 ms', '55']; + const metrics = ['80 ms', '4 ms', '76 ms', '55']; verifyClientMetrics(metrics, false); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 0b4dcea5d12e0..b6924b9552699 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -7,7 +7,13 @@ import * as React from 'react'; import numeral from '@elastic/numeral'; import styled from 'styled-components'; import { useContext, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiToolTip, + EuiIconTip, +} from '@elastic/eui'; import { useFetcher } from '../../../../hooks/useFetcher'; import { I18LABELS } from '../translations'; import { useUxQuery } from '../hooks/useUxQuery'; @@ -70,11 +76,35 @@ export function ClientMetrics() { return ( + + + {I18LABELS.totalPageLoad} + + + } + isLoading={status !== 'success'} + /> + + {I18LABELS.backEnd} + + + } isLoading={status !== 'success'} /> @@ -82,7 +112,15 @@ export function ClientMetrics() { + {I18LABELS.frontEnd} + + + } isLoading={status !== 'success'} /> diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index b19adb12d02d4..e4e9109f007e7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -36,8 +36,7 @@ export function RumDashboard() {

- {I18LABELS.pageLoadDuration} ( - {getPercentileLabel(percentile!)}) + {I18LABELS.pageLoad} ({getPercentileLabel(percentile!)})

diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index e91f129195366..c7fe8e885020a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -5,15 +5,20 @@ */ import React from 'react'; -import { EuiFlexItem, EuiStat, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiStat, EuiFlexGroup, EuiIconTip } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { DATA_UNDEFINED_LABEL, FCP_LABEL, + FCP_TOOLTIP, LONGEST_LONG_TASK, + LONGEST_LONG_TASK_TOOLTIP, NO_OF_LONG_TASK, + NO_OF_LONG_TASK_TOOLTIP, SUM_LONG_TASKS, + SUM_LONG_TASKS_TOOLTIP, TBT_LABEL, + TBT_TOOLTIP, } from './translations'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUxQuery } from '../hooks/useUxQuery'; @@ -70,7 +75,12 @@ export function KeyUXMetrics({ data, loading }: Props) { + {FCP_LABEL} + + + } isLoading={loading} /> @@ -78,7 +88,12 @@ export function KeyUXMetrics({ data, loading }: Props) { + {TBT_LABEL} + + + } isLoading={loading} /> @@ -90,7 +105,15 @@ export function KeyUXMetrics({ data, loading }: Props) { ? numeral(longTaskData?.noOfLongTasks).format('0,0') : DATA_UNDEFINED_LABEL } - description={NO_OF_LONG_TASK} + description={ + <> + {NO_OF_LONG_TASK} + + + } isLoading={status !== 'success'} /> @@ -98,7 +121,15 @@ export function KeyUXMetrics({ data, loading }: Props) { + {LONGEST_LONG_TASK} + + + } isLoading={status !== 'success'} /> @@ -106,7 +137,15 @@ export function KeyUXMetrics({ data, loading }: Props) { + {SUM_LONG_TASKS} + + + } isLoading={status !== 'success'} /> diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx index 329339deb2f89..3a6323a747a70 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx @@ -19,7 +19,7 @@ describe('KeyUXMetrics', () => { status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText } = render( + const { getAllByText } = render( { /> ); - expect(getByText('Longest long task duration 271 ms')).toBeInTheDocument(); - expect(getByText('Total long tasks duration 520 ms')).toBeInTheDocument(); - expect(getByText('No. of long tasks 3')).toBeInTheDocument(); - expect(getByText('Total blocking time 271 ms')).toBeInTheDocument(); - expect(getByText('First contentful paint 1.27 s')).toBeInTheDocument(); + const checkText = (text: string) => { + return (content: any, node: any) => { + return node?.textContent?.includes(text); + }; + }; + + expect( + getAllByText(checkText('Longest long task duration271 ms'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('Total long tasks duration520 ms'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('No. of long tasks3'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('Total blocking time271 ms'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('First contentful paint1.27 s'))[0] + ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts index 5920dc92f558d..3795f2f102237 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts @@ -18,10 +18,26 @@ export const FCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fcp', { defaultMessage: 'First contentful paint', }); +export const FCP_TOOLTIP = i18n.translate( + 'xpack.apm.rum.coreVitals.fcpTooltip', + { + defaultMessage: + 'First contentful paint (FCP) focusses on the initial rendering and measures the time from when the page starts loading to when any part of the page’s content is displayed on the screen.', + } +); + export const TBT_LABEL = i18n.translate('xpack.apm.rum.coreVitals.tbt', { defaultMessage: 'Total blocking time', }); +export const TBT_TOOLTIP = i18n.translate( + 'xpack.apm.rum.coreVitals.tbtTooltip', + { + defaultMessage: + 'Total blocking time (TBT) is the sum of the blocking time (duration above 50 ms) for each long task that occurs between the First contentful paint and the time when the transaction is completed.', + } +); + export const NO_OF_LONG_TASK = i18n.translate( 'xpack.apm.rum.uxMetrics.noOfLongTasks', { @@ -29,6 +45,14 @@ export const NO_OF_LONG_TASK = i18n.translate( } ); +export const NO_OF_LONG_TASK_TOOLTIP = i18n.translate( + 'xpack.apm.rum.uxMetrics.noOfLongTasksTooltip', + { + defaultMessage: + 'The number of long tasks, a long task is defined as any user activity or browser task that monopolizes the UI thread for extended periods (greater than 50 milliseconds) and blocks other critical tasks (frame rate or input latency) from being executed.', + } +); + export const LONGEST_LONG_TASK = i18n.translate( 'xpack.apm.rum.uxMetrics.longestLongTasks', { @@ -36,6 +60,14 @@ export const LONGEST_LONG_TASK = i18n.translate( } ); +export const LONGEST_LONG_TASK_TOOLTIP = i18n.translate( + 'xpack.apm.rum.uxMetrics.longestLongTasksTooltip', + { + defaultMessage: + 'The duration of the longest long task, a long task is defined as any user activity or browser task that monopolizes the UI thread for extended periods (greater than 50 milliseconds) and blocks other critical tasks (frame rate or input latency) from being executed.', + } +); + export const SUM_LONG_TASKS = i18n.translate( 'xpack.apm.rum.uxMetrics.sumLongTasks', { @@ -43,6 +75,14 @@ export const SUM_LONG_TASKS = i18n.translate( } ); +export const SUM_LONG_TASKS_TOOLTIP = i18n.translate( + 'xpack.apm.rum.uxMetrics.sumLongTasksTooltip', + { + defaultMessage: + 'The total duration of long tasks, a long task is defined as any user activity or browser task that monopolizes the UI thread for extended periods (greater than 50 milliseconds) and blocks other critical tasks (frame rate or input latency) from being executed.', + } +); + export const getPercentileLabel = (value: number) => { if (value === 50) return I18LABELS.median; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index b7ecfc08db13b..75df1381d8a1d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -10,6 +10,9 @@ export const I18LABELS = { dataMissing: i18n.translate('xpack.apm.rum.dashboard.dataMissing', { defaultMessage: 'N/A', }), + totalPageLoad: i18n.translate('xpack.apm.rum.dashboard.totalPageLoad', { + defaultMessage: 'Total', + }), backEnd: i18n.translate('xpack.apm.rum.dashboard.backend', { defaultMessage: 'Backend', }), @@ -34,6 +37,9 @@ export const I18LABELS = { defaultMessage: 'Page load duration', } ), + pageLoad: i18n.translate('xpack.apm.rum.dashboard.pageLoad.label', { + defaultMessage: 'Page load', + }), pageLoadDistribution: i18n.translate( 'xpack.apm.rum.dashboard.pageLoadDistribution.label', { @@ -156,6 +162,21 @@ export const I18LABELS = { noData: i18n.translate('xpack.apm.ux.visitorBreakdown.noData', { defaultMessage: 'No data.', }), + // Helper tooltips + totalPageLoadTooltip: i18n.translate( + 'xpack.apm.rum.dashboard.tooltips.totalPageLoad', + { + defaultMessage: 'Total represents the full page load duration', + } + ), + frontEndTooltip: i18n.translate('xpack.apm.rum.dashboard.tooltips.frontEnd', { + defaultMessage: + 'Frontend time represents the total page load duration minus the backend time', + }), + backEndTooltip: i18n.translate('xpack.apm.rum.dashboard.tooltips.backEnd', { + defaultMessage: + 'Backend time represents time to first byte (TTFB), which is when the first response packet is received after the request has been made', + }), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 53dcd2f469148..b89c46f6e3fc5 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -22,9 +22,9 @@ Object { ], }, }, - "domInteractive": Object { + "totalPageLoadDuration": Object { "percentiles": Object { - "field": "transaction.marks.agent.domInteractive", + "field": "transaction.duration.us", "hdr": Object { "number_of_significant_value_digits": 3, }, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index da65e69e7eb7c..6685a60f84f05 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -8,8 +8,8 @@ import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { - TRANSACTION_DOM_INTERACTIVE, TRANSACTION_TIME_TO_FIRST_BYTE, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; export async function getClientMetrics({ @@ -37,18 +37,18 @@ export async function getClientMetrics({ exists: { field: 'transaction.marks.navigationTiming.fetchStart' }, }, aggs: { - backEnd: { + totalPageLoadDuration: { percentiles: { - field: TRANSACTION_TIME_TO_FIRST_BYTE, + field: TRANSACTION_DURATION, percents: [percentile], hdr: { number_of_significant_value_digits: 3, }, }, }, - domInteractive: { + backEnd: { percentiles: { - field: TRANSACTION_DOM_INTERACTIVE, + field: TRANSACTION_TIME_TO_FIRST_BYTE, percents: [percentile], hdr: { number_of_significant_value_digits: 3, @@ -64,17 +64,19 @@ export async function getClientMetrics({ const { apmEventClient } = setup; const response = await apmEventClient.search(params); const { - hasFetchStartField: { backEnd, domInteractive }, + hasFetchStartField: { backEnd, totalPageLoadDuration }, } = response.aggregations!; const pkey = percentile.toFixed(1); - // Divide by 1000 to convert ms into seconds + const totalPageLoadDurationValue = totalPageLoadDuration.values[pkey] ?? 0; + const totalPageLoadDurationValueMs = totalPageLoadDurationValue / 1000; // Microseconds to milliseconds + const backendValue = backEnd.values[pkey] ?? 0; + return { pageViews: { value: response.hits.total.value ?? 0 }, - backEnd: { value: backEnd.values[pkey] || 0 }, - frontEnd: { - value: (domInteractive.values[pkey] || 0) - (backEnd.values[pkey] || 0), - }, + totalPageLoadDuration: { value: totalPageLoadDurationValueMs }, + backEnd: { value: backendValue }, + frontEnd: { value: totalPageLoadDurationValueMs - backendValue }, }; } From 59662eefd2dbfd98eaae20f04fd8120e15872c20 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 29 Oct 2020 16:06:12 +0100 Subject: [PATCH 32/73] [Lens] Add loading indicator during debounce time (#80158) --- ...ressions-public.reactexpressionrenderer.md | 2 +- ...c.reactexpressionrendererprops.debounce.md | 11 +++++++ ...ons-public.reactexpressionrendererprops.md | 1 + src/plugins/expressions/public/public.api.md | 4 ++- .../public/react_expression_renderer.test.tsx | 33 +++++++++++++++++++ .../public/react_expression_renderer.tsx | 32 ++++++++++++++---- .../editor_frame/suggestion_panel.tsx | 14 +++----- 7 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md index 66c2e1e3c0c8d..32a7151578658 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md @@ -7,5 +7,5 @@ Signature: ```typescript -ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element +ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md new file mode 100644 index 0000000000000..3f7eb12fbb7a8 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ReactExpressionRendererProps](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) > [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) + +## ReactExpressionRendererProps.debounce property + +Signature: + +```typescript +debounce?: number; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md index 5622516530edd..e4980ce04b9e2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md @@ -16,6 +16,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams | --- | --- | --- | | [className](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.classname.md) | string | | | [dataAttrs](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.dataattrs.md) | string[] | | +| [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) | number | | | [expression](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.expression.md) | string | ExpressionAstExpression | | | [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | (event: ExpressionRendererEvent) => void | | | [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | 'xs' | 's' | 'm' | 'l' | 'xl' | | diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index fe95cf5eb0cda..68a3507bbf166 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -1039,7 +1039,7 @@ export interface Range { // Warning: (ae-missing-release-tag) "ReactExpressionRenderer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element; +export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element; // Warning: (ae-missing-release-tag) "ReactExpressionRendererProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1050,6 +1050,8 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { // (undocumented) dataAttrs?: string[]; // (undocumented) + debounce?: number; + // (undocumented) expression: string | ExpressionAstExpression; // (undocumented) onEvent?: (event: ExpressionRendererEvent) => void; diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index 7c1711f056d69..052c2a9f6a24a 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -113,6 +113,39 @@ describe('ExpressionRenderer', () => { instance.unmount(); }); + it('waits for debounce period if specified', () => { + jest.useFakeTimers(); + + const refreshSubject = new Subject(); + const loaderUpdate = jest.fn(); + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$: new Subject(), + data$: new Subject(), + loading$: new Subject(), + update: loaderUpdate, + destroy: jest.fn(), + }; + }); + + const instance = mount( + + ); + + instance.setProps({ expression: 'abc' }); + + expect(loaderUpdate).toHaveBeenCalledTimes(1); + + act(() => { + jest.runAllTimers(); + }); + + expect(loaderUpdate).toHaveBeenCalledTimes(2); + + instance.unmount(); + }); + it('should display a custom error message if the user provides one and then remove it after successful render', () => { const dataSubject = new Subject(); const data$ = dataSubject.asObservable().pipe(share()); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 99d170c96666d..fecebf36ab7e6 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -45,6 +45,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { * An observable which can be used to re-run the expression without destroying the component */ reload$?: Observable; + debounce?: number; } export type ReactExpressionRendererType = React.ComponentType; @@ -71,6 +72,7 @@ export const ReactExpressionRenderer = ({ expression, onEvent, reload$, + debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); @@ -85,12 +87,28 @@ export const ReactExpressionRenderer = ({ const errorRenderHandlerRef: React.MutableRefObject = useRef( null ); + const [debouncedExpression, setDebouncedExpression] = useState(expression); + useEffect(() => { + if (debounce === undefined) { + return; + } + const handler = setTimeout(() => { + setDebouncedExpression(expression); + }, debounce); + + return () => { + clearTimeout(handler); + }; + }, [expression, debounce]); + + const activeExpression = debounce !== undefined ? debouncedExpression : expression; + const waitingForDebounceToComplete = debounce !== undefined && expression !== debouncedExpression; /* eslint-disable react-hooks/exhaustive-deps */ // OK to ignore react-hooks/exhaustive-deps because options update is handled by calling .update() useEffect(() => { const subs: Subscription[] = []; - expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, expression, { + expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, activeExpression, { ...expressionLoaderOptions, // react component wrapper provides different // error handling api which is easier to work with from react @@ -146,21 +164,21 @@ export const ReactExpressionRenderer = ({ useEffect(() => { const subscription = reload$?.subscribe(() => { if (expressionLoaderRef.current) { - expressionLoaderRef.current.update(expression, expressionLoaderOptions); + expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions); } }); return () => subscription?.unsubscribe(); - }, [reload$, expression, ...Object.values(expressionLoaderOptions)]); + }, [reload$, activeExpression, ...Object.values(expressionLoaderOptions)]); // Re-fetch data automatically when the inputs change useShallowCompareEffect( () => { if (expressionLoaderRef.current) { - expressionLoaderRef.current.update(expression, expressionLoaderOptions); + expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions); } }, // when expression is changed by reference and when any other loaderOption is changed by reference - [{ expression, ...expressionLoaderOptions }] + [{ activeExpression, ...expressionLoaderOptions }] ); /* eslint-enable react-hooks/exhaustive-deps */ @@ -188,7 +206,9 @@ export const ReactExpressionRenderer = ({ return (
{state.isEmpty && } - {state.isLoading && } + {(state.isLoading || waitingForDebounceToComplete) && ( + + )} {!state.isLoading && state.error && renderError && diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 5e5e9cda954ee..63ee02ac0404d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -32,16 +32,11 @@ import { ReactExpressionRendererType, } from '../../../../../../src/plugins/expressions/public'; import { prependDatasourceExpression } from './expression_helpers'; -import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; const MAX_SUGGESTIONS_DISPLAYED = 5; -// TODO: Remove this when upstream fix is merged https://github.com/elastic/eui/issues/2329 -// eslint-disable-next-line -const EuiPanelFixed = EuiPanel as React.ComponentType; - export interface SuggestionPanelProps { activeDatasourceId: string | null; datasourceMap: Record; @@ -82,6 +77,7 @@ const PreviewRenderer = ({ className="lnsSuggestionPanel__expressionRenderer" padding="s" expression={expression} + debounce={2000} renderError={() => { return (
@@ -104,8 +100,6 @@ const PreviewRenderer = ({ ); }; -const DebouncedPreviewRenderer = debouncedComponent(PreviewRenderer, 2000); - const SuggestionPreview = ({ preview, ExpressionRenderer: ExpressionRendererComponent, @@ -126,7 +120,7 @@ const SuggestionPreview = ({ return (
- {preview.expression ? ( - {preview.title} )} - +
); From 995111ad8a4d74fe546f9c77627c865ce4d95747 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 29 Oct 2020 16:07:09 +0100 Subject: [PATCH 33/73] [Uptime] Use base path for screenshot url (#81930) --- .../monitor/synthetics/__tests__/executed_journey.test.tsx | 3 +++ .../components/monitor/synthetics/executed_journey.tsx | 1 + .../public/components/monitor/synthetics/executed_step.tsx | 3 +-- .../monitor/synthetics/step_screenshot_display.tsx | 6 ++++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx index 5ab815a3c0b5d..9fec9439b3ad5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx @@ -253,6 +253,9 @@ describe('ExecutedJourney component', () => { } } /> + `); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx index 2ffb3f0feb4dd..9a3e045017f9a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx @@ -79,6 +79,7 @@ export const ExecutedJourney: FC = ({ journey }) => ( {journey.steps.filter(isStepEnd).map((step, index) => ( ))} +
); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index 3c26ba12eea65..5966851973af2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -41,7 +41,7 @@ export const ExecutedStep: FC = ({ step, index }) => (
- +
@@ -87,6 +87,5 @@ export const ExecutedStep: FC = ({ step, index }) => (
- ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx index 2e8ad4bd0c9a8..b81cf6bc1ec1d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useContext, useEffect, useRef, useState, FC } from 'react'; import { useIntersection } from 'react-use'; -import { UptimeThemeContext } from '../../../contexts'; +import { UptimeSettingsContext, UptimeThemeContext } from '../../../contexts'; interface StepScreenshotDisplayProps { screenshotExists?: boolean; @@ -41,6 +41,8 @@ export const StepScreenshotDisplay: FC = ({ colors: { lightestShade: pageBackground }, } = useContext(UptimeThemeContext); + const { basePath } = useContext(UptimeSettingsContext); + const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); const [isOverlayOpen, setIsOverlayOpen] = useState(false); @@ -59,7 +61,7 @@ export const StepScreenshotDisplay: FC = ({ }, [hasIntersected, isIntersecting, setHasIntersected]); let content: JSX.Element | null = null; - const imgSrc = `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`; + const imgSrc = basePath + `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`; if (hasIntersected && screenshotExists) { content = ( <> From 3ee665683732da596fe32ba3194733d0fee5d2ad Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 29 Oct 2020 10:37:50 -0500 Subject: [PATCH 34/73] [deb/rpm] remove sysv (#74424) Co-authored-by: Elastic Machine Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/migration/migrate_8_0.asciidoc | 10 + docs/setup/install/deb-init.asciidoc | 20 -- docs/setup/install/deb.asciidoc | 8 - docs/setup/install/rpm-init.asciidoc | 20 -- docs/setup/install/rpm.asciidoc | 7 - docs/setup/start-stop.asciidoc | 21 +-- src/dev/build/tasks/os_packages/run_fpm.ts | 1 - .../{sysv => systemd}/etc/default/kibana | 0 .../service_templates/sysv/etc/init.d/kibana | 174 ------------------ 9 files changed, 12 insertions(+), 249 deletions(-) delete mode 100644 docs/setup/install/deb-init.asciidoc delete mode 100644 docs/setup/install/rpm-init.asciidoc rename src/dev/build/tasks/os_packages/service_templates/{sysv => systemd}/etc/default/kibana (100%) delete mode 100755 src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 0cb28ce0fb6e7..ef76121b21d29 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -167,4 +167,14 @@ The `xpack.` prefix has been removed for all telemetry configurations. *Impact:* For any configurations beginning with `xpack.telemetry`, remove the `xpack` prefix. Use {kibana-ref}/telemetry-settings-kbn.html#telemetry-general-settings[`telemetry.enabled`] instead. +[float] +=== SysV init support has been removed + +*Details:* +All supported operating systems support using systemd service files. Any system that doesn't already have service aliased to use kibana.service should use `systemctl start kibana.service` instead of the `service start kibana`. + +*Impact:* +Any installations using `.deb` or `.rpm` packages using SysV will need to migrate to systemd. + + // end::notable-breaking-changes[] diff --git a/docs/setup/install/deb-init.asciidoc b/docs/setup/install/deb-init.asciidoc deleted file mode 100644 index 6e21b8f97cf7e..0000000000000 --- a/docs/setup/install/deb-init.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -==== Run {kib} with SysV `init` - -Use the `update-rc.d` command to configure Kibana to start automatically -when the system boots up: - -[source,sh] --------------------------------------------------- -sudo update-rc.d kibana defaults 95 10 --------------------------------------------------- - -You can start and stop Kibana using the `service` command: - -[source,sh] --------------------------------------------- -sudo -i service kibana start -sudo -i service kibana stop --------------------------------------------- - -If Kibana fails to start for any reason, it will print the reason for -failure to `STDOUT`. Log files can be found in `/var/log/kibana/`. diff --git a/docs/setup/install/deb.asciidoc b/docs/setup/install/deb.asciidoc index 234c02cee0be1..c830faf1432d7 100644 --- a/docs/setup/install/deb.asciidoc +++ b/docs/setup/install/deb.asciidoc @@ -160,15 +160,7 @@ https://artifacts.elastic.co/downloads/kibana/kibana-oss-{version}-amd64.deb endif::[] -==== SysV `init` vs `systemd` - -include::init-systemd.asciidoc[] - -[[deb-running-init]] -include::deb-init.asciidoc[] - [[deb-running-systemd]] - include::systemd.asciidoc[] [[deb-configuring]] diff --git a/docs/setup/install/rpm-init.asciidoc b/docs/setup/install/rpm-init.asciidoc deleted file mode 100644 index 08282635a014f..0000000000000 --- a/docs/setup/install/rpm-init.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -==== Run {kib} with SysV `init` - -Use the `chkconfig` command to configure Kibana to start automatically -when the system boots up: - -[source,sh] --------------------------------------------------- -sudo chkconfig --add kibana --------------------------------------------------- - -You can start and stop Kibana using the `service` command: - -[source,sh] --------------------------------------------- -sudo -i service kibana start -sudo -i service kibana stop --------------------------------------------- - -If Kibana fails to start for any reason, it will print the reason for -failure to `STDOUT`. Log files can be found in `/var/log/kibana/`. diff --git a/docs/setup/install/rpm.asciidoc b/docs/setup/install/rpm.asciidoc index 1153353aa9a0f..0b63684808d7d 100644 --- a/docs/setup/install/rpm.asciidoc +++ b/docs/setup/install/rpm.asciidoc @@ -153,13 +153,6 @@ https://artifacts.elastic.co/downloads/kibana/kibana-oss-{version}-x86_64.rpm endif::[] -==== SysV `init` vs `systemd` - -include::init-systemd.asciidoc[] - -[[rpm-running-init]] -include::rpm-init.asciidoc[] - [[rpm-running-systemd]] include::systemd.asciidoc[] diff --git a/docs/setup/start-stop.asciidoc b/docs/setup/start-stop.asciidoc index 198bc76bbb400..8952cd3a23cd3 100644 --- a/docs/setup/start-stop.asciidoc +++ b/docs/setup/start-stop.asciidoc @@ -25,25 +25,8 @@ stop and start {kib} from the command line. include::install/windows-running.asciidoc[] [float] -[[start-stop-deb]] -=== Debian packages - -include::install/init-systemd.asciidoc[] - -[float] -include::install/deb-init.asciidoc[] - -[float] -include::install/systemd.asciidoc[] - -[float] -[[start-stop-rpm]] -=== RPM packages - -include::install/init-systemd.asciidoc[] - -[float] -include::install/rpm-init.asciidoc[] +[[start-stop-deb-rpm]] +=== Debian and RPM packages [float] include::install/systemd.asciidoc[] diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index b8289f1da194f..e5de760ea11d0 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -134,7 +134,6 @@ export async function runFpm( `${resolveWithTrailingSlash(fromBuild('data'))}=/var/lib/kibana/`, // copy package configurations - `${resolveWithTrailingSlash(__dirname, 'service_templates/sysv/')}=/`, `${resolveWithTrailingSlash(__dirname, 'service_templates/systemd/')}=/`, ]; diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana similarity index 100% rename from src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana rename to src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana deleted file mode 100755 index eedd4898ce6c3..0000000000000 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ /dev/null @@ -1,174 +0,0 @@ -#!/bin/sh -# Init script for kibana -# Maintained by -# Generated by pleaserun. -# Implemented based on LSB Core 3.1: -# * Sections: 20.2, 20.3 -# -### BEGIN INIT INFO -# Provides: kibana -# Required-Start: $remote_fs $syslog -# Required-Stop: $remote_fs $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: -# Description: Kibana -### END INIT INFO - -# -# Source function libraries if present. -# (It improves integration with systemd) -# -# Red Hat -if [ -f /etc/rc.d/init.d/functions ]; then - . /etc/rc.d/init.d/functions - -# Debian -elif [ -f /lib/lsb/init-functions ]; then - . /lib/lsb/init-functions - -# SUSE -elif [ -f /etc/rc.status ]; then - . /etc/rc.status - rc_reset -fi - -name=kibana -program=/usr/share/kibana/bin/kibana -args="--logging.dest=/var/log/kibana/kibana.log" -pidfile="/var/run/kibana/$name.pid" - -[ -r /etc/default/$name ] && . /etc/default/$name -[ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name - -export KBN_PATH_CONF -export NODE_OPTIONS - -[ -z "$nice" ] && nice=0 - -trace() { - logger -t "/etc/init.d/kibana" "$@" -} - -emit() { - trace "$@" - echo "$@" -} - -start() { - [ ! -d "/var/run/kibana/" ] && mkdir "/var/run/kibana/" - chown "$user":"$group" "/var/run/kibana/" - chmod 755 "/var/run/kibana/" - - chroot --userspec "$user":"$group" "$chroot" sh -c " - - cd \"$chdir\" - exec \"$program $args\" - " >> /var/log/kibana/kibana.log 2>&1 & - - # Generate the pidfile from here. If we instead made the forked process - # generate it there will be a race condition between the pidfile writing - # and a process possibly asking for status. - echo $! > $pidfile - - emit "$name started" - return 0 -} - -stop() { - # Try a few times to kill TERM the program - if status ; then - pid=$(cat "$pidfile") - trace "Killing $name (pid $pid) with SIGTERM" - kill -TERM $pid - # Wait for it to exit. - for i in 1 2 3 4 5 ; do - trace "Waiting $name (pid $pid) to die..." - status || break - sleep 1 - done - if status ; then - if [ "$KILL_ON_STOP_TIMEOUT" -eq 1 ] ; then - trace "Timeout reached. Killing $name (pid $pid) with SIGKILL. This may result in data loss." - kill -KILL $pid - emit "$name killed with SIGKILL." - else - emit "$name stop failed; still running." - fi - else - emit "$name stopped." - fi - fi -} - -status() { - if [ -f "$pidfile" ] ; then - pid=$(cat "$pidfile") - if ps -p $pid > /dev/null 2> /dev/null ; then - # process by this pid is running. - # It may not be our pid, but that's what you get with just pidfiles. - # TODO(sissel): Check if this process seems to be the same as the one we - # expect. It'd be nice to use flock here, but flock uses fork, not exec, - # so it makes it quite awkward to use in this case. - return 0 - else - return 2 # program is dead but pid file exists - fi - else - return 3 # program is not running - fi -} - -force_stop() { - if status ; then - stop - status && kill -KILL $(cat "$pidfile") - fi -} - - -case "$1" in - force-start|start|stop|force-stop|restart) - trace "Attempting '$1' on kibana" - ;; -esac - -case "$1" in - force-start) - PRESTART=no - exec "$0" start - ;; - start) - status - code=$? - if [ $code -eq 0 ]; then - emit "$name is already running" - exit $code - else - start - exit $? - fi - ;; - stop) stop ;; - force-stop) force_stop ;; - status) - status - code=$? - if [ $code -eq 0 ] ; then - emit "$name is running" - else - emit "$name is not running" - fi - exit $code - ;; - restart) - - stop && start - ;; - *) - echo "Usage: $SCRIPTNAME {start|force-start|stop|force-start|force-stop|status|restart}" >&2 - exit 3 - ;; -esac - -exit $? From 6deafd06b84d4c94ead42e0860e75d39a0c70ca0 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 29 Oct 2020 16:43:22 +0100 Subject: [PATCH 35/73] [Search] Use session service on a dashboard (#81297) --- ...ugins-embeddable-public.embeddableinput.md | 1 + .../application/dashboard_app_controller.tsx | 17 +++---- .../embeddable/dashboard_container.test.tsx | 22 +++++++++ .../embeddable/dashboard_container.tsx | 12 ++++- .../data/public/search/expressions/esaggs.ts | 7 ++- .../embeddable/search_embeddable.ts | 9 +++- src/plugins/embeddable/common/types.ts | 5 ++ .../lib/embeddables/embeddable.test.tsx | 15 ++++++ .../public/lib/embeddables/embeddable.tsx | 7 +-- src/plugins/embeddable/public/public.api.md | 1 + .../common/adapters/request/types.ts | 1 + .../requests/components/requests_view.tsx | 15 ++++++ .../public/embeddable/visualize_embeddable.ts | 1 + .../embeddable/embeddable.test.tsx | 11 ++++- .../embeddable/embeddable.tsx | 1 + .../embeddable/expression_wrapper.tsx | 3 ++ .../dashboard/async_search/async_search.ts | 46 ++++++++++++++++-- .../dashboard/async_search/data.json | 48 +++++++++++++++++++ 18 files changed, 202 insertions(+), 20 deletions(-) diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md index d1d97d50f5948..f36f7b4ee77a4 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md @@ -19,5 +19,6 @@ export declare type EmbeddableInput = { timeRange?: TimeRange; query?: Query; filters?: Filter[]; + searchSessionId?: string; }; ``` diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index fa45e433050ab..e5947b73b305b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -139,7 +139,7 @@ export class DashboardAppController { dashboardCapabilities, scopedHistory, embeddableCapabilities: { visualizeCapabilities, mapsCapabilities }, - data: { query: queryService }, + data: { query: queryService, search: searchService }, core: { notifications, overlays, @@ -412,8 +412,9 @@ export class DashboardAppController { >(DASHBOARD_CONTAINER_TYPE); if (dashboardFactory) { + const searchSessionId = searchService.session.start(); dashboardFactory - .create(getDashboardInput()) + .create({ ...getDashboardInput(), searchSessionId }) .then((container: DashboardContainer | ErrorEmbeddable | undefined) => { if (container && !isErrorEmbeddable(container)) { dashboardContainer = container; @@ -572,7 +573,7 @@ export class DashboardAppController { differences.filters = appStateDashboardInput.filters; } - Object.keys(_.omit(containerInput, ['filters'])).forEach((key) => { + Object.keys(_.omit(containerInput, ['filters', 'searchSessionId'])).forEach((key) => { const containerValue = (containerInput as { [key: string]: unknown })[key]; const appStateValue = ((appStateDashboardInput as unknown) as { [key: string]: unknown })[ key @@ -590,7 +591,8 @@ export class DashboardAppController { const refreshDashboardContainer = () => { const changes = getChangesFromAppStateForContainerState(); if (changes && dashboardContainer) { - dashboardContainer.updateInput(changes); + const searchSessionId = searchService.session.start(); + dashboardContainer.updateInput({ ...changes, searchSessionId }); } }; @@ -1109,12 +1111,6 @@ export class DashboardAppController { $scope.model.filters = filterManager.getFilters(); $scope.model.query = queryStringManager.getQuery(); dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); - if (dashboardContainer) { - dashboardContainer.updateInput({ - filters: $scope.model.filters, - query: $scope.model.query, - }); - } }, }); @@ -1159,6 +1155,7 @@ export class DashboardAppController { if (dashboardContainer) { dashboardContainer.destroy(); } + searchService.session.clear(); }); } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx index a7226082d3dce..89aacf2a84029 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx @@ -134,3 +134,25 @@ test('Container view mode change propagates to new children', async () => { expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); }); + +test('searchSessionId propagates to children', async () => { + const searchSessionId1 = 'searchSessionId1'; + const container = new DashboardContainer( + getSampleDashboardInput({ searchSessionId: searchSessionId1 }), + options + ); + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Bob', + }); + + expect(embeddable.getInput().searchSessionId).toBe(searchSessionId1); + + const searchSessionId2 = 'searchSessionId2'; + container.updateInput({ searchSessionId: searchSessionId2 }); + + expect(embeddable.getInput().searchSessionId).toBe(searchSessionId2); +}); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 036880a1d088b..757488185fe8e 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -78,6 +78,7 @@ export interface InheritedChildInput extends IndexSignature { viewMode: ViewMode; hidePanelTitles?: boolean; id: string; + searchSessionId?: string; } export interface DashboardContainerOptions { @@ -228,7 +229,15 @@ export class DashboardContainer extends Container { // Create a new search source that inherits the original search source // but has the appropriate timeRange applied via a filter. @@ -143,6 +145,7 @@ const handleCourierRequest = async ({ defaultMessage: 'This request queries Elasticsearch to fetch the data for the visualization.', }), + searchSessionId, } ); request.stats(getRequestInspectorStats(requestSearchSource)); @@ -150,6 +153,7 @@ const handleCourierRequest = async ({ try { const response = await requestSearchSource.fetch({ abortSignal, + sessionId: searchSessionId, }); request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); @@ -248,7 +252,7 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ multi: true, }, }, - async fn(input, args, { inspectorAdapters, abortSignal }) { + async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { const indexPatterns = getIndexPatterns(); const { filterManager } = getQueryService(); const searchService = getSearchService(); @@ -276,6 +280,7 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ inspectorAdapters: inspectorAdapters as Adapters, filterManager, abortSignal: (abortSignal as unknown) as AbortSignal, + searchSessionId: getSearchSessionId(), }); const table: Datatable = { diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index af88cacfcf992..170078076ec6f 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -266,6 +266,8 @@ export class SearchEmbeddable } private fetch = async () => { + const searchSessionId = this.input.searchSessionId; + if (!this.searchScope) return; const { searchSource } = this.savedSearch; @@ -292,7 +294,11 @@ export class SearchEmbeddable const description = i18n.translate('discover.embeddable.inspectorRequestDescription', { defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', }); - const inspectorRequest = this.inspectorAdaptors.requests.start(title, { description }); + + const inspectorRequest = this.inspectorAdaptors.requests.start(title, { + description, + searchSessionId, + }); inspectorRequest.stats(getRequestInspectorStats(searchSource)); searchSource.getSearchRequestBody().then((body: Record) => { inspectorRequest.json(body); @@ -303,6 +309,7 @@ export class SearchEmbeddable // Make the request const resp = await searchSource.fetch({ abortSignal: this.abortController.signal, + sessionId: searchSessionId, }); this.updateOutput({ loading: false, error: undefined }); diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 68b842c934de8..2737f2678ff32 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -67,4 +67,9 @@ export type EmbeddableInput = { * Visualization filters used to narrow down results. */ filters?: Filter[]; + + /** + * Search session id to group searches + */ + searchSessionId?: string; }; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx index 340d851f3eedf..b020006c0c2bb 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx @@ -25,6 +25,7 @@ import { EmbeddableOutput, EmbeddableInput } from './i_embeddable'; import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples/embeddables/contact_card/contact_card_embeddable'; import { FilterableEmbeddable } from '../test_samples/embeddables/filterable_embeddable'; +import type { Filter } from '../../../../data/public'; class TestClass { constructor() {} @@ -79,6 +80,20 @@ test('Embeddable reload is called if lastReloadRequest input time changes', asyn expect(hello.reload).toBeCalledTimes(1); }); +test('Embeddable reload is called if lastReloadRequest input time changed and new input is used', async () => { + const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 0 }); + + const aFilter = ({} as unknown) as Filter; + hello.reload = jest.fn(() => { + // when reload is called embeddable already has new input + expect(hello.getInput().filters).toEqual([aFilter]); + }); + + hello.updateInput({ lastReloadRequestTime: 1, filters: [aFilter] }); + + expect(hello.reload).toBeCalledTimes(1); +}); + test('Embeddable reload is not called if lastReloadRequest input time does not change', async () => { const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 1 }); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 9267d600360cf..c7afc157c1452 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -195,14 +195,15 @@ export abstract class Embeddable< private onResetInput(newInput: TEmbeddableInput) { if (!isEqual(this.input, newInput)) { - if (this.input.lastReloadRequestTime !== newInput.lastReloadRequestTime) { - this.reload(); - } + const oldLastReloadRequestTime = this.input.lastReloadRequestTime; this.input = newInput; this.input$.next(newInput); this.updateOutput({ title: getPanelTitle(this.input, this.output), } as Partial); + if (oldLastReloadRequestTime !== newInput.lastReloadRequestTime) { + this.reload(); + } } } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 84dd97c8288fc..9939ba2a0f8a1 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -425,6 +425,7 @@ export type EmbeddableInput = { timeRange?: TimeRange; query?: Query; filters?: Filter[]; + searchSessionId?: string; }; // Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/inspector/common/adapters/request/types.ts b/src/plugins/inspector/common/adapters/request/types.ts index 0a62df7c8d10c..c08c2ef6f706f 100644 --- a/src/plugins/inspector/common/adapters/request/types.ts +++ b/src/plugins/inspector/common/adapters/request/types.ts @@ -49,6 +49,7 @@ export interface Request extends RequestParams { export interface RequestParams { id?: string; description?: string; + searchSessionId?: string; } export interface RequestStatistics { diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index a433ea70dc35c..13575de0c5064 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -153,6 +153,21 @@ export class RequestsViewComponent extends Component )} + {this.state.request && this.state.request.searchSessionId && ( + +

+ +

+
+ )} + {this.state.request && } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index a810b4b65528f..c12a0f0759018 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -374,6 +374,7 @@ export class VisualizeEmbeddable query: this.input.query, filters: this.input.filters, }, + searchSessionId: this.input.searchSessionId, uiState: this.vis.uiState, inspectorAdapters: this.inspectorAdapters, }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 3e05d4ddfbc20..9dc59eacd40d3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -172,6 +172,7 @@ describe('embeddable', () => { timeRange, query, filters, + searchSessionId: 'searchSessionId', }); expect(expressionRenderer).toHaveBeenCalledTimes(2); @@ -182,7 +183,13 @@ describe('embeddable', () => { const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; - const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput; + const input = { + savedObjectId: '123', + timeRange, + query, + filters, + searchSessionId: 'searchSessionId', + } as LensEmbeddableInput; const embeddable = new Embeddable( { @@ -214,6 +221,8 @@ describe('embeddable', () => { filters, }) ); + + expect(expressionRenderer.mock.calls[0][0].searchSessionId).toBe(input.searchSessionId); }); it('should merge external context with query and filters of the saved object', async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index d245b7f2fcde4..10c243a272138 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -177,6 +177,7 @@ export class Embeddable ExpressionRenderer={this.expressionRenderer} expression={this.expression || null} searchContext={this.getMergedSearchContext()} + searchSessionId={this.input.searchSessionId} handleEvent={this.handleEvent} />, domNode diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index 4fb0630a305e7..13376e56e2144 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -19,6 +19,7 @@ export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; expression: string | null; searchContext: ExecutionContextSearch; + searchSessionId?: string; handleEvent: (event: ExpressionRendererEvent) => void; } @@ -27,6 +28,7 @@ export function ExpressionWrapper({ expression, searchContext, handleEvent, + searchSessionId, }: ExpressionWrapperProps) { return ( @@ -51,6 +53,7 @@ export function ExpressionWrapper({ padding="m" expression={expression} searchContext={searchContext} + searchSessionId={searchSessionId} renderError={(errorMessage, error) => (
diff --git a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts index 6932a88635a67..4d37ee1589169 100644 --- a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts +++ b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts @@ -12,6 +12,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']); + const dashboardPanelActions = getService('dashboardPanelActions'); + const inspector = getService('inspector'); + const queryBar = getService('queryBar'); describe('dashboard with async search', () => { before(async function () { @@ -24,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('not delayed should load', async () => { await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('Not Delayed'); + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('embeddableErrorLabel'); const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); @@ -33,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('delayed should load', async () => { await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('Delayed 5s'); + await PageObjects.dashboard.loadSavedDashboard('Delayed 5s'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('embeddableErrorLabel'); const data = await PageObjects.visChart.getBarChartData(''); @@ -42,10 +45,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('timed out should show error', async () => { await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('Delayed 15s'); + await PageObjects.dashboard.loadSavedDashboard('Delayed 15s'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('embeddableErrorLabel'); await testSubjects.existOrFail('searchTimeoutError'); }); + + it('multiple searches are grouped and only single error popup is shown', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Multiple delayed'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('embeddableErrorLabel'); + // there should be two failed panels + expect((await testSubjects.findAll('embeddableErrorLabel')).length).to.be(2); + // but only single error toast because searches are grouped + expect((await testSubjects.findAll('searchTimeoutError')).length).to.be(1); + + // check that session ids are the same + const getSearchSessionIdByPanel = async (panelTitle: string) => { + await dashboardPanelActions.openInspectorByTitle(panelTitle); + await inspector.openInspectorRequestsView(); + const searchSessionId = await ( + await testSubjects.find('inspectorRequestSearchSessionId') + ).getAttribute('data-search-session-id'); + await inspector.close(); + return searchSessionId; + }; + + const panel1SessionId1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + const panel2SessionId1 = await getSearchSessionIdByPanel( + 'Sum of Bytes by Extension (Delayed 5s)' + ); + expect(panel1SessionId1).to.be(panel2SessionId1); + + await queryBar.clickQuerySubmitButton(); + + const panel1SessionId2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + const panel2SessionId2 = await getSearchSessionIdByPanel( + 'Sum of Bytes by Extension (Delayed 5s)' + ); + expect(panel1SessionId2).to.be(panel2SessionId2); + expect(panel1SessionId1).not.to.be(panel1SessionId2); + }); }); } diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/data.json b/x-pack/test/functional/es_archives/dashboard/async_search/data.json index 2990097e88d00..486c73f711a6b 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/data.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/data.json @@ -194,4 +194,52 @@ } } +{ + "type": "doc", + "value": { + "id": "dashboard:a41c6790-075d-11eb-be70-0bd5e8b57d03", + "index": ".kibana", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "optionsJSON": "{\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"ec585931-ce8e-43fd-aa94-a1a9612d24ba\"},\"panelIndex\":\"ec585931-ce8e-43fd-aa94-a1a9612d24ba\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"c7b18010-462b-4e55-a974-fdec2ae64b06\"},\"panelIndex\":\"c7b18010-462b-4e55-a974-fdec2ae64b06\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"e67704f7-20b7-4ade-8dee-972a9d187107\"},\"panelIndex\":\"e67704f7-20b7-4ade-8dee-972a9d187107\",\"embeddableConfig\":{},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"f0b03592-10f1-41cd-9929-0cb4163bcd16\"},\"panelIndex\":\"f0b03592-10f1-41cd-9929-0cb4163bcd16\",\"embeddableConfig\":{},\"panelRefName\":\"panel_3\"}]", + "refreshInterval": { "pause": true, "value": 0 }, + "timeFrom": "2015-09-19T17:34:10.297Z", + "timeRestore": true, + "timeTo": "2015-09-23T00:09:17.180Z", + "title": "Multiple delayed", + "version": 1 + }, + "references": [ + { + "id": "14501a50-01e3-11eb-9b63-176d7b28a352", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "50a67010-075d-11eb-be70-0bd5e8b57d02", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6c9f3830-01e3-11eb-9b63-176d7b28a352", + "name": "panel_2", + "type": "visualization" + }, + { + "id": "50a67010-075d-11eb-be70-0bd5e8b57d02", + "name": "panel_3", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-03-19T11:59:53.701Z" + } + } +} From afd5fe3b8a197ee0177a661fa65f816031708970 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 29 Oct 2020 16:47:34 +0100 Subject: [PATCH 36/73] Date column utilities (#81007) --- packages/kbn-optimizer/limits.yml | 2 +- .../common/search/aggs/aggs_service.test.ts | 5 +- .../data/common/search/aggs/aggs_service.ts | 20 ++- .../search/aggs/buckets/date_histogram.ts | 21 +-- src/plugins/data/common/search/aggs/types.ts | 15 +++ .../data/common/search/aggs/utils/index.ts | 1 + .../search/aggs/utils/infer_time_zone.test.ts | 79 ++++++++++++ .../search/aggs/utils/infer_time_zone.ts | 46 +++++++ .../aggs/utils/time_column_meta.test.ts | 121 ++++++++++++++++++ .../search/aggs/utils/time_column_meta.ts | 66 ++++++++++ src/plugins/data/public/public.api.md | 3 +- .../public/search/aggs/aggs_service.test.ts | 5 +- .../data/public/search/aggs/aggs_service.ts | 17 ++- src/plugins/data/public/search/aggs/mocks.ts | 1 + .../data/public/search/expressions/esaggs.ts | 7 + .../data/public/search/search_service.ts | 2 +- .../data/server/index_patterns/mocks.ts | 2 +- .../server/search/aggs/aggs_service.test.ts | 2 + .../data/server/search/aggs/aggs_service.ts | 19 ++- src/plugins/data/server/search/aggs/mocks.ts | 1 + .../data/server/search/search_service.ts | 2 +- src/plugins/data/server/server.api.md | 1 + src/plugins/embeddable/public/public.api.md | 1 + .../snapshots/baseline/combined_test2.json | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/partial_test_3.json | 2 +- .../snapshots/baseline/step_output_test2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../snapshots/session/combined_test2.json | 2 +- .../snapshots/session/combined_test3.json | 2 +- .../snapshots/session/final_output_test.json | 2 +- .../snapshots/session/metric_all_data.json | 2 +- .../session/metric_multi_metric_data.json | 2 +- .../session/metric_percentage_mode.json | 2 +- .../session/metric_single_metric_data.json | 2 +- .../snapshots/session/partial_test_1.json | 2 +- .../snapshots/session/partial_test_2.json | 2 +- .../snapshots/session/partial_test_3.json | 2 +- .../snapshots/session/step_output_test2.json | 2 +- .../snapshots/session/step_output_test3.json | 2 +- .../snapshots/session/tagcloud_all_data.json | 2 +- .../snapshots/session/tagcloud_fontsize.json | 2 +- .../session/tagcloud_metric_data.json | 2 +- .../snapshots/session/tagcloud_options.json | 2 +- 55 files changed, 436 insertions(+), 67 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts create mode 100644 src/plugins/data/common/search/aggs/utils/infer_time_zone.ts create mode 100644 src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts create mode 100644 src/plugins/data/common/search/aggs/utils/time_column_meta.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c660d37222504..3f9fdb164e759 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -14,7 +14,7 @@ pageLoadAssetSize: dashboard: 374194 dashboardEnhanced: 65646 dashboardMode: 22716 - data: 1287839 + data: 1317839 dataEnhanced: 50420 devTools: 38637 discover: 105145 diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index bcf2101704c80..160860bcce591 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -44,6 +44,8 @@ describe('Aggs service', () => { }; startDeps = { getConfig: jest.fn(), + getIndexPattern: jest.fn(), + isDefaultTimezone: jest.fn(), }; }); @@ -201,8 +203,9 @@ describe('Aggs service', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(3); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); + expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); }); diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index 6f3e3904dbbd5..b6afa708f9e6f 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -18,7 +18,7 @@ */ import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; -import { UI_SETTINGS } from '../../../common'; +import { IndexPattern, UI_SETTINGS } from '../../../common'; import { GetConfigFn } from '../../types'; import { AggConfigs, @@ -28,6 +28,7 @@ import { getCalculateAutoTimeExpression, } from './'; import { AggsCommonSetup, AggsCommonStart } from './types'; +import { getDateMetaByDatatableColumn } from './utils/time_column_meta'; /** @internal */ export const aggsRequiredUiSettings = [ @@ -50,6 +51,8 @@ export interface AggsCommonSetupDependencies { /** @internal */ export interface AggsCommonStartDependencies { getConfig: GetConfigFn; + getIndexPattern(id: string): Promise; + isDefaultTimezone: () => boolean; } /** @@ -77,11 +80,22 @@ export class AggsCommonService { }; } - public start({ getConfig }: AggsCommonStartDependencies): AggsCommonStart { + public start({ + getConfig, + getIndexPattern, + isDefaultTimezone, + }: AggsCommonStartDependencies): AggsCommonStart { const aggTypesStart = this.aggTypesRegistry.start(); + const calculateAutoTimeExpression = getCalculateAutoTimeExpression(getConfig); return { - calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + calculateAutoTimeExpression, + getDateMetaByDatatableColumn: getDateMetaByDatatableColumn({ + calculateAutoTimeExpression, + getIndexPattern, + getConfig, + isDefaultTimezone, + }), createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: aggTypesStart, diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index c273ca53a5fed..694b03f660452 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -34,6 +34,7 @@ import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; import { BaseAggParams } from '../types'; import { dateHistogramInterval } from '../utils'; +import { inferTimeZone } from '../utils'; /** @internal */ export type CalculateBoundsFn = (timeRange: TimeRange) => TimeRangeBounds; @@ -235,25 +236,7 @@ export const getDateHistogramBucketAgg = ({ // time_zones being persisted into saved_objects serialize: noop, write(agg, output) { - // If a time_zone has been set explicitly always prefer this. - let tz = agg.params.time_zone; - if (!tz && agg.params.field) { - // If a field has been configured check the index pattern's typeMeta if a date_histogram on that - // field requires a specific time_zone - tz = get(agg.getIndexPattern(), [ - 'typeMeta', - 'aggs', - 'date_histogram', - agg.params.field.name, - 'time_zone', - ]); - } - if (!tz) { - // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz - const detectedTimezone = moment.tz.guess(); - const tzOffset = moment().format('Z'); - tz = isDefaultTimezone() ? detectedTimezone || tzOffset : getConfig('dateFormat:tz'); - } + const tz = inferTimeZone(agg.params, agg.getIndexPattern(), isDefaultTimezone, getConfig); output.params.time_zone = tz; }, }, diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index aec3dcc9d068c..09a13762d4d70 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -18,7 +18,9 @@ */ import { Assign } from '@kbn/utility-types'; +import { DatatableColumn } from 'src/plugins/expressions'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; +import { TimeRange } from '../../query'; import { AggConfigSerialized, AggConfigs, @@ -80,6 +82,19 @@ export interface AggsCommonSetup { /** @internal */ export interface AggsCommonStart { calculateAutoTimeExpression: ReturnType; + /** + * Helper function returning meta data about use date intervals for a data table column. + * If the column is not a column created by a date histogram aggregation of the esaggs data source, + * this function will return undefined. + * + * Otherwise, it will return the following attributes in an object: + * * `timeZone` time zone used to create the buckets (important e.g. for DST), + * * `timeRange` total time range of the fetch data (to infer partial buckets at the beginning and end of the data) + * * `interval` Interval used on elasticsearch (`auto` resolved to the actual interval) + */ + getDateMetaByDatatableColumn: ( + column: DatatableColumn + ) => Promise; createAggConfigs: ( indexPattern: IndexPattern, configStates?: CreateAggConfigParams[], diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index 99ce44207d80d..7d6cb1c7ef33f 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -23,3 +23,4 @@ export * from './get_format_with_aggs'; export * from './ipv4_address'; export * from './prop_filter'; export * from './to_angular_json'; +export * from './infer_time_zone'; diff --git a/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts b/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts new file mode 100644 index 0000000000000..8fc3726ee1b32 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('moment', () => { + const moment: any = jest.fn(() => { + return { + format: jest.fn(() => '-1;00'), + }; + }); + moment.tz = { + guess: jest.fn(() => 'CET'), + }; + return moment; +}); + +import { IndexPattern } from '../../../index_patterns'; +import { AggParamsDateHistogram } from '../buckets'; +import { inferTimeZone } from './infer_time_zone'; + +describe('inferTimeZone', () => { + it('reads time zone from agg params', () => { + const params: AggParamsDateHistogram = { + time_zone: 'CEST', + }; + expect(inferTimeZone(params, {} as IndexPattern, () => false, jest.fn())).toEqual('CEST'); + }); + + it('reads time zone from index pattern type meta if available', () => { + expect( + inferTimeZone( + { field: 'mydatefield' }, + ({ + typeMeta: { + aggs: { + date_histogram: { + mydatefield: { + time_zone: 'UTC', + }, + }, + }, + }, + } as unknown) as IndexPattern, + () => false, + jest.fn() + ) + ).toEqual('UTC'); + }); + + it('reads time zone from moment if set to default', () => { + expect(inferTimeZone({}, {} as IndexPattern, () => true, jest.fn())).toEqual('CET'); + }); + + it('reads time zone from config if not set to default', () => { + expect( + inferTimeZone( + {}, + {} as IndexPattern, + () => false, + () => 'CET' as any + ) + ).toEqual('CET'); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts b/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts new file mode 100644 index 0000000000000..282238c5a0459 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import { IndexPattern } from '../../../index_patterns'; +import { AggParamsDateHistogram } from '../buckets'; + +export function inferTimeZone( + params: AggParamsDateHistogram, + indexPattern: IndexPattern, + isDefaultTimezone: () => boolean, + getConfig: (key: string) => T +) { + let tz = params.time_zone; + if (!tz && params.field) { + // If a field has been configured check the index pattern's typeMeta if a date_histogram on that + // field requires a specific time_zone + tz = indexPattern.typeMeta?.aggs?.date_histogram?.[params.field]?.time_zone; + } + if (!tz) { + // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz + const detectedTimezone = moment.tz.guess(); + const tzOffset = moment().format('Z'); + tz = isDefaultTimezone() + ? detectedTimezone || tzOffset + : // if timezone is not the default, this will always return a string + (getConfig('dateFormat:tz') as string); + } + return tz; +} diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts new file mode 100644 index 0000000000000..e56d622734554 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BUCKET_TYPES } from '../buckets'; +import { DateMetaByColumnDeps, getDateMetaByDatatableColumn } from './time_column_meta'; + +describe('getDateMetaByDatatableColumn', () => { + let params: DateMetaByColumnDeps; + beforeEach(() => { + params = { + calculateAutoTimeExpression: jest.fn().mockReturnValue('5m'), + getIndexPattern: jest.fn().mockResolvedValue({}), + isDefaultTimezone: jest.fn().mockReturnValue(true), + getConfig: jest.fn(), + }; + }); + + it('returns nothing on column from other data source', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'essql', + }, + }) + ).toEqual(undefined); + }); + + it('returns nothing on non date histogram column', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.TERMS, + }, + }, + }) + ).toEqual(undefined); + }); + + it('returns time range, time zone and interval', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.DATE_HISTOGRAM, + params: { + time_zone: 'UTC', + interval: '1h', + }, + appliedTimeRange: { + from: 'now-5d', + to: 'now', + }, + }, + }, + }) + ).toEqual({ + timeZone: 'UTC', + timeRange: { + from: 'now-5d', + to: 'now', + }, + interval: '1h', + }); + }); + + it('returns resolved auto interval', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.DATE_HISTOGRAM, + params: { + time_zone: 'UTC', + interval: 'auto', + }, + appliedTimeRange: { + from: 'now-5d', + to: 'now', + }, + }, + }, + }) + ).toEqual( + expect.objectContaining({ + interval: '5m', + }) + ); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts new file mode 100644 index 0000000000000..1bea716c6a049 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DatatableColumn } from 'src/plugins/expressions/common'; +import { IndexPattern } from '../../../index_patterns'; + +import { TimeRange } from '../../../types'; +import { AggParamsDateHistogram, BUCKET_TYPES } from '../buckets'; +import { inferTimeZone } from './infer_time_zone'; + +export interface DateMetaByColumnDeps { + calculateAutoTimeExpression: (range: TimeRange) => string | undefined; + getIndexPattern: (id: string) => Promise; + isDefaultTimezone: () => boolean; + getConfig: (key: string) => T; +} + +export const getDateMetaByDatatableColumn = ({ + calculateAutoTimeExpression, + getIndexPattern, + isDefaultTimezone, + getConfig, +}: DateMetaByColumnDeps) => async ( + column: DatatableColumn +): Promise => { + if (column.meta.source !== 'esaggs') return; + if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) return; + const params = column.meta.sourceParams.params as AggParamsDateHistogram; + const appliedTimeRange = column.meta.sourceParams.appliedTimeRange as TimeRange; + + const tz = inferTimeZone( + params, + await getIndexPattern(column.meta.sourceParams.indexPatternId as string), + isDefaultTimezone, + getConfig + ); + + const interval = + params.interval === 'auto' ? calculateAutoTimeExpression(appliedTimeRange) : params.interval; + + if (!interval) { + throw new Error('time interval could not be determined'); + } + + return { + timeZone: tz, + timeRange: appliedTimeRange, + interval, + }; +}; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 81fa6d4ba20db..7ee21236c1c79 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -16,6 +16,7 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; +import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; @@ -2285,7 +2286,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/aggs/types.ts:113:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index db25dfb300d11..de747d234b441 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -23,6 +23,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks'; import { BucketAggType, getAggTypes, MetricAggType } from '../../../common'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; +import { dataPluginMock } from '../../mocks'; import { AggsService, @@ -46,6 +47,7 @@ describe('AggsService - public', () => { }; startDeps = { fieldFormats: fieldFormatsServiceMock.createStartContract(), + indexPatterns: dataPluginMock.createStartContract().indexPatterns, uiSettings, }; }); @@ -86,8 +88,9 @@ describe('AggsService - public', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(3); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); + expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); }); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 4b088ddfe314f..85e0f604bb8b5 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -32,6 +32,7 @@ import { AggTypesDependencies, } from '../../../common/search/aggs'; import { AggsSetup, AggsStart } from './types'; +import { IndexPatternsContract } from '../../index_patterns'; /** * Aggs needs synchronous access to specific uiSettings. Since settings can change @@ -68,6 +69,7 @@ export interface AggsSetupDependencies { export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; uiSettings: IUiSettingsClient; + indexPatterns: IndexPatternsContract; } /** @@ -94,9 +96,17 @@ export class AggsService { return this.aggsCommonService.setup({ registerFunction }); } - public start({ fieldFormats, uiSettings }: AggsStartDependencies): AggsStart { - const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ + public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { + const isDefaultTimezone = () => uiSettings.isDefault('dateFormat:tz'); + + const { + calculateAutoTimeExpression, + getDateMetaByDatatableColumn, + types, + } = this.aggsCommonService.start({ getConfig: this.getConfig!, + getIndexPattern: indexPatterns.get, + isDefaultTimezone, }); const aggTypesDependencies: AggTypesDependencies = { @@ -106,7 +116,7 @@ export class AggsService { deserialize: fieldFormats.deserialize, getDefaultInstance: fieldFormats.getDefaultInstance, }), - isDefaultTimezone: () => uiSettings.isDefault('dateFormat:tz'), + isDefaultTimezone, }; // initialize each agg type and store in memory @@ -137,6 +147,7 @@ export class AggsService { return { calculateAutoTimeExpression, + getDateMetaByDatatableColumn, createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); }, diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index ca13343777e63..abc930f00b594 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -67,6 +67,7 @@ export const searchAggsSetupMock = (): AggsSetup => ({ export const searchAggsStartMock = (): AggsStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + getDateMetaByDatatableColumn: jest.fn(), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 0aab345a4ebc0..dba77d398c8b6 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -298,6 +298,13 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ source: 'esaggs', sourceParams: { indexPatternId: indexPattern.id, + appliedTimeRange: + column.aggConfig.params.field?.name && + input?.timeRange && + args.timeFields && + args.timeFields.includes(column.aggConfig.params.field?.name) + ? { from: input.timeRange.from, to: input.timeRange.to } + : undefined, ...column.aggConfig.serialize(), }, }, diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index f955dc5b6ebd5..3dbabfc68fdbc 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -143,7 +143,7 @@ export class SearchService implements Plugin { }; return { - aggs: this.aggsService.start({ fieldFormats, uiSettings }), + aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), search, showError: (e: Error) => { this.searchInterceptor.showError(e); diff --git a/src/plugins/data/server/index_patterns/mocks.ts b/src/plugins/data/server/index_patterns/mocks.ts index 8f95afe3b3c9d..52d8aa1e35093 100644 --- a/src/plugins/data/server/index_patterns/mocks.ts +++ b/src/plugins/data/server/index_patterns/mocks.ts @@ -19,6 +19,6 @@ export function createIndexPatternsStartMock() { return { - indexPatternsServiceFactory: jest.fn(), + indexPatternsServiceFactory: jest.fn().mockResolvedValue({ get: jest.fn() }), }; } diff --git a/src/plugins/data/server/search/aggs/aggs_service.test.ts b/src/plugins/data/server/search/aggs/aggs_service.test.ts index d9a945a15fb67..cb4239cc339c4 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.test.ts @@ -23,6 +23,7 @@ import { coreMock } from '../../../../../core/server/mocks'; import { expressionsPluginMock } from '../../../../../plugins/expressions/server/mocks'; import { BucketAggType, getAggTypes, MetricAggType } from '../../../common'; import { createFieldFormatsStartMock } from '../../field_formats/mocks'; +import { createIndexPatternsStartMock } from '../../index_patterns/mocks'; import { AggsService, AggsSetupDependencies, AggsStartDependencies } from './aggs_service'; @@ -40,6 +41,7 @@ describe('AggsService - server', () => { }; startDeps = { fieldFormats: createFieldFormatsStartMock(), + indexPatterns: createIndexPatternsStartMock(), uiSettings, }; }); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index 3e5cd8adb44a6..c805c8af6694c 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -30,6 +30,7 @@ import { TimeRange, } from '../../../common'; import { FieldFormatsStart } from '../../field_formats'; +import { IndexPatternsServiceStart } from '../../index_patterns'; import { AggsSetup, AggsStart } from './types'; /** @internal */ @@ -41,6 +42,7 @@ export interface AggsSetupDependencies { export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; uiSettings: UiSettingsServiceStart; + indexPatterns: IndexPatternsServiceStart; } /** @@ -61,7 +63,7 @@ export class AggsService { return this.aggsCommonService.setup({ registerFunction }); } - public start({ fieldFormats, uiSettings }: AggsStartDependencies): AggsStart { + public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { return { asScopedToClient: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); @@ -72,8 +74,18 @@ export class AggsService { const getConfig = (key: string): T => { return uiSettingsCache[key]; }; + const isDefaultTimezone = () => getConfig('dateFormat:tz') === 'Browser'; - const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ getConfig }); + const { + calculateAutoTimeExpression, + getDateMetaByDatatableColumn, + types, + } = this.aggsCommonService.start({ + getConfig, + getIndexPattern: (await indexPatterns.indexPatternsServiceFactory(savedObjectsClient)) + .get, + isDefaultTimezone, + }); const aggTypesDependencies: AggTypesDependencies = { calculateBounds: this.calculateBounds, @@ -87,7 +99,7 @@ export class AggsService { * default timezone, but `isDefault` is not currently offered on the * server, so we need to manually check for the default value. */ - isDefaultTimezone: () => getConfig('dateFormat:tz') === 'Browser', + isDefaultTimezone, }; const typesRegistry = { @@ -109,6 +121,7 @@ export class AggsService { return { calculateAutoTimeExpression, + getDateMetaByDatatableColumn, createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); }, diff --git a/src/plugins/data/server/search/aggs/mocks.ts b/src/plugins/data/server/search/aggs/mocks.ts index b50e22fe87b7c..be060de73b9ff 100644 --- a/src/plugins/data/server/search/aggs/mocks.ts +++ b/src/plugins/data/server/search/aggs/mocks.ts @@ -68,6 +68,7 @@ export const searchAggsSetupMock = (): AggsSetup => ({ const commonStartMock = (): AggsCommonStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + getDateMetaByDatatableColumn: jest.fn(), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 0130d3aacc91f..04ee0e95c7f08 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -149,7 +149,7 @@ export class SearchService implements Plugin { { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { return { - aggs: this.aggsService.start({ fieldFormats, uiSettings }), + aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), getSearchStrategy: this.getSearchStrategy, search: this.search.bind(this), searchSource: { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index e5882a6cff809..a3edbbd3844b3 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -13,6 +13,7 @@ import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CoreStart } from 'src/core/server'; import { CoreStart as CoreStart_2 } from 'kibana/server'; +import { DatatableColumn } from 'src/plugins/expressions'; import { Duration } from 'moment'; import { ElasticsearchClient } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 9939ba2a0f8a1..00971ed37db3a 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -17,6 +17,7 @@ import { CoreSetup as CoreSetup_2 } from 'src/core/public'; import { CoreSetup as CoreSetup_3 } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'kibana/public'; import * as CSS from 'csstype'; +import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; import { EmbeddableStart as EmbeddableStart_2 } from 'src/plugins/embeddable/public/plugin'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index cf488ac7f3ffa..dd779800cd452 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 8c272901c4e84..992d667fdce9f 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index abc0d3a446987..031c9f9ea5504 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index 1809df5e709f0..8c6fde201c8f1 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index ec32b07ed9f2e..14c8428c6d432 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index 09602eca4abf2..595127526156e 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 071172c698ad7..073fca760b9a2 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index ad38bb28b3329..93f8d8a27d233 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 997285adfe5f4..e8c47efdbe622 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 10e23d860637c..38683082975f8 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index cf488ac7f3ffa..dd779800cd452 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 8c272901c4e84..992d667fdce9f 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index abc0d3a446987..031c9f9ea5504 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index 1809df5e709f0..8c6fde201c8f1 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index ec32b07ed9f2e..14c8428c6d432 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index 09602eca4abf2..595127526156e 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 071172c698ad7..073fca760b9a2 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index ad38bb28b3329..93f8d8a27d233 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 997285adfe5f4..e8c47efdbe622 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index 10e23d860637c..38683082975f8 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file From 4434c393350ec41f84dce0d3567f0fd52a1dde79 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Thu, 29 Oct 2020 16:02:32 +0000 Subject: [PATCH 37/73] fix toast message (#81687) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../detection_engine/rules/api.test.ts | 14 +- .../containers/detection_engine/rules/api.ts | 18 +- .../detection_engine/rules/translations.ts | 16 +- .../rules/use_pre_packaged_rules.test.tsx | 274 +++++++++++++++++- .../rules/use_pre_packaged_rules.tsx | 34 ++- 5 files changed, 338 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 8076733be2d7d..0b708133d947b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -411,7 +411,12 @@ describe('Detections Rules API', () => { describe('createPrepackagedRules', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue('unknown'); + fetchMock.mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); }); test('check parameter url when creating pre-packaged rules', async () => { @@ -423,7 +428,12 @@ describe('Detections Rules API', () => { }); test('happy path', async () => { const resp = await createPrepackagedRules({ signal: abortCtrl.signal }); - expect(resp).toEqual(true); + expect(resp).toEqual({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 23adfe0228333..ce1fdd18dbdef 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -245,13 +245,25 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => { - await KibanaServices.get().http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, { +export const createPrepackagedRules = async ({ + signal, +}: BasicFetchProps): Promise<{ + rules_installed: number; + rules_updated: number; + timelines_installed: number; + timelines_updated: number; +}> => { + const result = await KibanaServices.get().http.fetch<{ + rules_installed: number; + rules_updated: number; + timelines_installed: number; + timelines_updated: number; + }>(DETECTION_ENGINE_PREPACKAGED_URL, { method: 'PUT', signal, }); - return true; + return result; }; /** diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts index 721790a36b27f..6e2aee9658658 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts @@ -30,7 +30,21 @@ export const RULE_AND_TIMELINE_PREPACKAGED_FAILURE = i18n.translate( export const RULE_AND_TIMELINE_PREPACKAGED_SUCCESS = i18n.translate( 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleAndTimelineSuccesDescription', { - defaultMessage: 'Installed pre-packaged rules and timelines from elastic', + defaultMessage: 'Installed pre-packaged rules and timeline templates from elastic', + } +); + +export const RULE_PREPACKAGED_SUCCESS = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription', + { + defaultMessage: 'Installed pre-packaged rules from elastic', + } +); + +export const TIMELINE_PREPACKAGED_SUCCESS = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedTimelineSuccesDescription', + { + defaultMessage: 'Installed pre-packaged timeline templates from elastic', } ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 7f74e92584494..f6bd8c4359d6e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -3,12 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { ReactElement } from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { ReturnPrePackagedRulesAndTimelines, usePrePackagedRules } from './use_pre_packaged_rules'; import * as api from './api'; +import { shallow } from 'enzyme'; +import * as i18n from './translations'; -jest.mock('./api'); +jest.mock('./api', () => ({ + getPrePackagedRulesStatus: jest.fn(), + createPrepackagedRules: jest.fn(), +})); describe('usePrePackagedRules', () => { beforeEach(() => { @@ -52,6 +57,21 @@ describe('usePrePackagedRules', () => { }); test('fetch getPrePackagedRulesStatus', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -87,7 +107,6 @@ describe('usePrePackagedRules', () => { }); test('happy path to createPrePackagedRules', async () => { - const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -106,7 +125,7 @@ describe('usePrePackagedRules', () => { resp = await result.current.createPrePackagedRules(); } expect(resp).toEqual(true); - expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); + expect(api.createPrepackagedRules).toHaveBeenCalled(); expect(result.current).toEqual({ getLoadPrebuiltRulesAndTemplatesButton: result.current.getLoadPrebuiltRulesAndTemplatesButton, @@ -127,6 +146,253 @@ describe('usePrePackagedRules', () => { }); }); + test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_RULES', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + 'data-test-subj': 'button', + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="button"]').text()).toEqual(i18n.LOAD_PREPACKAGED_RULES); + }); + }); + + test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_TIMELINE_TEMPLATES', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + 'data-test-subj': 'button', + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="button"]').text()).toEqual( + i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES + ); + }); + }); + + test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_RULES_AND_TEMPLATES', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + 'data-test-subj': 'button', + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="button"]').text()).toEqual( + i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES + ); + }); + }); + + test('getReloadPrebuiltRulesAndTemplatesButton - missing rules and templates', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 1, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual( + 'Install 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline ' + ); + }); + }); + + test('getReloadPrebuiltRulesAndTemplatesButton - missing rules', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 1, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual( + 'Install 1 Elastic prebuilt rule ' + ); + }); + }); + + test('getReloadPrebuiltRulesAndTemplatesButton - missing templates', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 1, + rules_not_installed: 0, + rules_not_updated: 0, + timelines_installed: 1, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual( + 'Install 1 Elastic prebuilt timeline ' + ); + }); + }); + test('unhappy path to createPrePackagedRules', async () => { const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); spyOnCreatePrepackagedRules.mockImplementation(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 4d19f44bcfc84..48530ddeb181e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -114,6 +114,27 @@ export const usePrePackagedRules = ({ const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); + const getSuccessToastMessage = (result: { + rules_installed: number; + rules_updated: number; + timelines_installed: number; + timelines_updated: number; + }) => { + const { + rules_installed: rulesInstalled, + rules_updated: rulesUpdated, + timelines_installed: timelinesInstalled, + timelines_updated: timelinesUpdated, + } = result; + if (rulesInstalled === 0 && (timelinesInstalled > 0 || timelinesUpdated > 0)) { + return i18n.TIMELINE_PREPACKAGED_SUCCESS; + } else if ((rulesInstalled > 0 || rulesUpdated > 0) && timelinesInstalled === 0) { + return i18n.RULE_PREPACKAGED_SUCCESS; + } else { + return i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS; + } + }; + useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); @@ -170,7 +191,7 @@ export const usePrePackagedRules = ({ isSignalIndexExists ) { setLoadingCreatePrePackagedRules(true); - await createPrepackagedRules({ + const result = await createPrepackagedRules({ signal: abortCtrl.signal, }); @@ -209,11 +230,7 @@ export const usePrePackagedRules = ({ timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed, timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated, }); - - displaySuccessToast( - i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS, - dispatchToaster - ); + displaySuccessToast(getSuccessToastMessage(result), dispatchToaster); stopTimeOut(); resolve(true); } else { @@ -277,8 +294,9 @@ export const usePrePackagedRules = ({ ); const getLoadPrebuiltRulesAndTemplatesButton = useCallback( ({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => { - return prePackagedRuleStatus === 'ruleNotInstalled' || - prePackagedTimelineStatus === 'timelinesNotInstalled' ? ( + return (prePackagedRuleStatus === 'ruleNotInstalled' || + prePackagedTimelineStatus === 'timelinesNotInstalled') && + prePackagedRuleStatus !== 'someRuleUninstall' ? ( Date: Thu, 29 Oct 2020 11:23:39 -0500 Subject: [PATCH 38/73] Service overview tab and route (#81972) Placeholder tab and route for service overview page. Fixes #81718. --- .../app/Main/route_config/index.tsx | 14 + .../app/ServiceDetails/ServiceDetailTabs.tsx | 16 +- .../components/app/service_overview/index.tsx | 246 ++++++++++++++++++ .../service_overview.test.tsx | 29 +++ .../Links/apm/service_overview_link.tsx | 23 ++ 5 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 0d61ca8e39845..f96dc14e34264 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -92,6 +92,12 @@ function ServiceDetailsNodes( return ; } +function ServiceDetailsOverview( + props: RouteComponentProps<{ serviceName: string }> +) { + return ; +} + function ServiceDetailsServiceMap( props: RouteComponentProps<{ serviceName: string }> ) { @@ -215,6 +221,14 @@ export const routes: APMRouteDefinition[] = [ `/services/${props.match.params.serviceName}/transactions` )(props), } as APMRouteDefinition<{ serviceName: string }>, + { + exact: true, + path: '/services/:serviceName/overview', + breadcrumb: i18n.translate('xpack.apm.breadcrumb.overviewTitle', { + defaultMessage: 'Overview', + }), + component: ServiceDetailsOverview, + } as APMRouteDefinition<{ serviceName: string }>, // errors { exact: true, diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 76c8a289b830c..d51e4a2dd3d7c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -16,16 +16,24 @@ import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; import { MetricOverviewLink } from '../../shared/Links/apm/MetricOverviewLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { ServiceNodeOverviewLink } from '../../shared/Links/apm/ServiceNodeOverviewLink'; +import { ServiceOverviewLink } from '../../shared/Links/apm/service_overview_link'; import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; import { ErrorGroupOverview } from '../ErrorGroupOverview'; import { ServiceMap } from '../ServiceMap'; import { ServiceMetrics } from '../ServiceMetrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; +import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../TransactionOverview'; interface Props { serviceName: string; - tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map'; + tab: + | 'errors' + | 'metrics' + | 'nodes' + | 'overview' + | 'service-map' + | 'transactions'; } export function ServiceDetailTabs({ serviceName, tab }: Props) { @@ -34,13 +42,13 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const overviewTab = { link: ( - + {i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', { defaultMessage: 'Overview', })} - + ), - render: () => <>, + render: () => , name: 'overview', }; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx new file mode 100644 index 0000000000000..81f23b6427508 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { useTrackPageview } from '../../../../../observability/public'; +import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; +import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; +import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; + +const rowHeight = 310; +const latencyChartRowHeight = 230; + +const Row = styled(EuiFlexItem)` + height: ${rowHeight}px; +`; + +const LatencyChartRow = styled(EuiFlexItem)` + height: ${latencyChartRowHeight}px; +`; + +const TableLinkFlexItem = styled(EuiFlexItem)` + & > a { + text-align: right; + } +`; + +interface ServiceOverviewProps { + serviceName: string; +} + +export function ServiceOverview({ serviceName }: ServiceOverviewProps) { + useTrackPageview({ app: 'apm', path: 'service_overview' }); + useTrackPageview({ app: 'apm', path: 'service_overview', delay: 15000 }); + + return ( + + + + + Search bar + + + Comparison picker + + + Date picker + + + + + + +

+ {i18n.translate('xpack.apm.serviceOverview.latencyChartTitle', { + defaultMessage: 'Latency', + })} +

+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.trafficChartTitle', + { + defaultMessage: 'Traffic', + } + )} +

+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableTitle', + { + defaultMessage: 'Transactions', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableLinkText', + { + defaultMessage: 'View transactions', + } + )} + + +
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.errorRateChartTitle', + { + defaultMessage: 'Error rate', + } + )} +

+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.errorsTableTitle', + { + defaultMessage: 'Errors', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.errorsTableLinkText', + { + defaultMessage: 'View errors', + } + )} + + +
+
+
+
+
+ + + + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.averageDurationBySpanTypeChartTitle', + { + defaultMessage: 'Average duration by span type', + } + )} +

+
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableTitle', + { + defaultMessage: 'Dependencies', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableLinkText', + { + defaultMessage: 'View service map', + } + )} + + +
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle', + { + defaultMessage: 'Instances latency distribution', + } + )} +

+
+
+
+ + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.instancesTableTitle', + { + defaultMessage: 'Instances', + } + )} +

+
+
+
+
+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx new file mode 100644 index 0000000000000..4e2063930a9c9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { ServiceOverview } from './'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} + +describe('ServiceOverview', () => { + it('renders', () => { + expect(() => + render(, { + wrapper: Wrapper, + }) + ).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx new file mode 100644 index 0000000000000..5d7859e7362c7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { APMLink, APMLinkExtendProps } from './APMLink'; + +interface ServiceOverviewLinkProps extends APMLinkExtendProps { + serviceName: string; +} + +export function ServiceOverviewLink({ + serviceName, + ...rest +}: ServiceOverviewLinkProps) { + return ; +} From 84b23b6d7ca021d2674563d7c6cb2ec6c33d08b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 29 Oct 2020 12:39:20 -0400 Subject: [PATCH 39/73] Move task manager README.md to root of plugin (#82012) * Move task manager README.md to root of plugin * Fix failing test, update task manager plugin description in docs --- docs/developer/plugin-list.asciidoc | 4 ++-- x-pack/plugins/task_manager/{server => }/README.md | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) rename x-pack/plugins/task_manager/{server => }/README.md (99%) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 8e08c3806446d..914bd8fce6dff 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -488,8 +488,8 @@ the alertTypes by the Stack in the alerting plugin, register associated HTTP routes, etc. -|{kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/task_manager/README.md[taskManager] +|The task manager is a generic system for running background tasks. |{kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] diff --git a/x-pack/plugins/task_manager/server/README.md b/x-pack/plugins/task_manager/README.md similarity index 99% rename from x-pack/plugins/task_manager/server/README.md rename to x-pack/plugins/task_manager/README.md index a0b35ad094537..b25d3cc49f980 100644 --- a/x-pack/plugins/task_manager/server/README.md +++ b/x-pack/plugins/task_manager/README.md @@ -1,7 +1,8 @@ # Kibana task manager -The task manager is a generic system for running background tasks. It supports: +The task manager is a generic system for running background tasks. +It supports: - Single-run and recurring tasks - Scheduling tasks to run after a specified datetime - Basic retry logic From 052f8277fea0df42e41cab0dc021b7ef7bddc893 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 29 Oct 2020 10:56:42 -0700 Subject: [PATCH 40/73] Add READMEs for ES UI plugins (#81973) * Add Search Profiler README. * Add Upgrade Assistant README. * Add name of plugin to Watcher README. * Add Console Extensions README. * Add Grok Debugger README. * Add Painless Lab README. * Add License Management README. * Add Remote Clusters README. * Add Console README. --- docs/developer/plugin-list.asciidoc | 37 +++++++----- src/plugins/console/README.md | 5 ++ x-pack/plugins/console_extensions/README.md | 3 + x-pack/plugins/grokdebugger/README.md | 6 ++ x-pack/plugins/license_management/README.md | 5 ++ x-pack/plugins/painless_lab/README.md | 5 ++ x-pack/plugins/remote_clusters/README.md | 5 ++ x-pack/plugins/searchprofiler/README.md | 8 +++ x-pack/plugins/upgrade_assistant/README.md | 67 +++++++++++++++++++++ x-pack/plugins/watcher/README.md | 4 +- 10 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 src/plugins/console/README.md create mode 100644 x-pack/plugins/console_extensions/README.md create mode 100644 x-pack/plugins/grokdebugger/README.md create mode 100644 x-pack/plugins/license_management/README.md create mode 100644 x-pack/plugins/painless_lab/README.md create mode 100644 x-pack/plugins/remote_clusters/README.md create mode 100644 x-pack/plugins/searchprofiler/README.md create mode 100644 x-pack/plugins/upgrade_assistant/README.md diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 914bd8fce6dff..f30afce3ee02c 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -38,8 +38,8 @@ NOTE: |The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. -|{kib-repo}blob/{branch}/src/plugins/console[console] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/console/README.md[console] +|Console provides the user with tools for storing and executing requests against Elasticsearch. |<> @@ -307,8 +307,8 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error |WARNING: Missing README. -|{kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/console_extensions/README.md[consoleExtensions] +|This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. |{kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] @@ -376,8 +376,9 @@ or dashboards from the Kibana instance, from both server and client-side plugins |This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. -|{kib-repo}blob/{branch}/x-pack/plugins/grokdebugger[grokdebugger] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/grokdebugger/README.md[grokdebugger] +|This plugin helps users define Grok patterns, +which are particularly useful for ingesting logs. |{kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] @@ -406,8 +407,8 @@ the infrastructure monitoring use-case within Kibana. |Run all tests from the x-pack root directory -|{kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/license_management/README.md[licenseManagement] +|This plugin enables users to activate a trial license, downgrade to Basic, and upload a new license. |{kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] @@ -444,12 +445,12 @@ Elastic. |This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. -|{kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/painless_lab/README.md[painlessLab] +|This plugin helps users learn how to use the Painless scripting language. -|{kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/remote_clusters/README.md[remoteClusters] +|This plugin helps users manage their remote clusters, which enable cross-cluster search and cross-cluster replication. |{kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] @@ -460,8 +461,11 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. -|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler/README.md[searchprofiler] +|The search profiler consumes the Profile API +by sending a search API with profile: true enabled in the request body. The response contains +detailed information on how Elasticsearch executed the search request. People use this information +to understand why a search request might be slow. |{kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] @@ -513,8 +517,9 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona |Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. -|{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant/README.md[upgradeAssistant] +|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary +purposes are to: |{kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] diff --git a/src/plugins/console/README.md b/src/plugins/console/README.md new file mode 100644 index 0000000000000..07421151f8087 --- /dev/null +++ b/src/plugins/console/README.md @@ -0,0 +1,5 @@ +# Console + +## About + +Console provides the user with tools for storing and executing requests against Elasticsearch. \ No newline at end of file diff --git a/x-pack/plugins/console_extensions/README.md b/x-pack/plugins/console_extensions/README.md new file mode 100644 index 0000000000000..49d83d2888d6b --- /dev/null +++ b/x-pack/plugins/console_extensions/README.md @@ -0,0 +1,3 @@ +# Console extensions + +This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. \ No newline at end of file diff --git a/x-pack/plugins/grokdebugger/README.md b/x-pack/plugins/grokdebugger/README.md new file mode 100644 index 0000000000000..80729b35500b6 --- /dev/null +++ b/x-pack/plugins/grokdebugger/README.md @@ -0,0 +1,6 @@ +# Grok Debugger + +## About + +This plugin helps users define [Grok patterns](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html), +which are particularly useful for ingesting logs. \ No newline at end of file diff --git a/x-pack/plugins/license_management/README.md b/x-pack/plugins/license_management/README.md new file mode 100644 index 0000000000000..b103c8fcd6721 --- /dev/null +++ b/x-pack/plugins/license_management/README.md @@ -0,0 +1,5 @@ +# License Management + +## About + +This plugin enables users to activate a trial license, downgrade to Basic, and upload a new license. \ No newline at end of file diff --git a/x-pack/plugins/painless_lab/README.md b/x-pack/plugins/painless_lab/README.md new file mode 100644 index 0000000000000..519b6a9dea8ed --- /dev/null +++ b/x-pack/plugins/painless_lab/README.md @@ -0,0 +1,5 @@ +# Painless Lab + +## About + +This plugin helps users learn how to use the [Painless scripting language](https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-painless.html). \ No newline at end of file diff --git a/x-pack/plugins/remote_clusters/README.md b/x-pack/plugins/remote_clusters/README.md new file mode 100644 index 0000000000000..1119c98ffe84a --- /dev/null +++ b/x-pack/plugins/remote_clusters/README.md @@ -0,0 +1,5 @@ +# Remote Clusters + +## About + +This plugin helps users manage their [remote clusters](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-remote-clusters.html), which enable cross-cluster search and cross-cluster replication. \ No newline at end of file diff --git a/x-pack/plugins/searchprofiler/README.md b/x-pack/plugins/searchprofiler/README.md new file mode 100644 index 0000000000000..6e25163470488 --- /dev/null +++ b/x-pack/plugins/searchprofiler/README.md @@ -0,0 +1,8 @@ +# Search Profiler + +## About + +The search profiler consumes the [Profile API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html) +by sending a `search` API with `profile: true` enabled in the request body. The response contains +detailed information on how Elasticsearch executed the search request. People use this information +to understand why a search request might be slow. \ No newline at end of file diff --git a/x-pack/plugins/upgrade_assistant/README.md b/x-pack/plugins/upgrade_assistant/README.md new file mode 100644 index 0000000000000..a6cb3b431c82b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/README.md @@ -0,0 +1,67 @@ +# Upgrade Assistant + +## About + +Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary +purposes are to: + +* **Surface deprecations.** Deprecations are features that are currently being used that will be +removed in the next major. Surfacing tells the user that there's a problem preventing them +from upgrading. +* **Migrate from deprecation features to supported features.** This addresses the problem, clearing +the path for the upgrade. Generally speaking, once all deprecations are addressed, the user can +safely upgrade. + +### Deprecations + +There are two sources of deprecation information: + +* [**Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/master/migration-api-deprecation.html) +This is information about cluster, node, and index level settings that use deprecated features that +will be removed or changed in the next major version. Currently, only cluster and index deprecations +will be surfaced in the Upgrade Assistant. ES server engineers are responsible for adding +deprecations to the Deprecation Info API. +* [**Deprecation logs.**](https://www.elastic.co/guide/en/elasticsearch/reference/current/logging.html#deprecation-logging) +These surface runtime deprecations, e.g. a Painless script that uses a deprecated accessor or a +request to a deprecated API. These are also generally surfaced as deprecation headers within the +response. Even if the cluster state is good, app maintainers need to watch the logs in case +deprecations are discovered as data is migrated. + +### Fixing problems + +Problems can be fixed at various points in the upgrade process. The Upgrade Assistant supports +various upgrade paths and surfaces various types of upgrade-related issues. + +* **Fixing deprecated cluster settings pre-upgrade.** This generally requires fixing some settings +in `elasticsearch.yml`. +* **Migrating indices data pre-upgrade.** This can involve deleting indices so that ES can rebuild +them in the new version, reindexing them so that they're built using a new Lucene version, or +applying a migration script that reindexes them with new settings/mappings/etc. +* **Migrating indices data post-upgrade.** As was the case with APM in the 6.8->7.x upgrade, +sometimes the new data format isn't forwards-compatible. In these cases, the user will perform the +upgrade first and then use the Upgrade Assistant to reindex their data to be compatible with the new +version. + +Deprecations can be handled in a number of ways: + +* **Reindexing.** When a user's index contains deprecations (e.g. mappings) a reindex solves them. +Upgrade Assistant contains migration scripts that are executed as part of the reindex process. +The user will see a "Reindex" button they can click which will apply this script and perform the +reindex. + * Reindexing is an atomic process in Upgrade Assistant, so that ingestion is never disrupted. + It works like this: + * Create a new index with a "reindexed-" prefix ([#30114](https://github.com/elastic/kibana/pull/30114)). + * Create an index alias pointing from the original index name to the prefixed index name. + * Reindex from the original index into the prefixed index. + * Delete the old index and rename the prefixed index. + * Some apps might require custom scripts, as was the case with APM ([#29845](https://github.com/elastic/kibana/pull/29845)). + In that case the migration performed a reindex with a Painless script (covered by automated tests) + that made the required changes to the data. +* **Update index settings.** Some index settings will need to be updated, which doesn't require a +reindex. An example of this is the "Fix" button that was added for metricbeat and filebeat indices +([#32829](https://github.com/elastic/kibana/pull/32829), [#33439](https://github.com/elastic/kibana/pull/33439)). +* **Following the docs.** The Deprecation Info API provides links to the deprecation docs. Users +will follow these docs to address the problem and make these warnings or errors disappear in the +Upgrade Assistant. +* **Stopping/restarting tasks and jobs.** Users had to stop watches and ML jobs and restart them as +soon as reindexing was complete ([#29663](https://github.com/elastic/kibana/pull/29663)). \ No newline at end of file diff --git a/x-pack/plugins/watcher/README.md b/x-pack/plugins/watcher/README.md index 4f9111760a0a6..c849a65019174 100644 --- a/x-pack/plugins/watcher/README.md +++ b/x-pack/plugins/watcher/README.md @@ -1,4 +1,4 @@ -# Conventions +# Watcher This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): @@ -69,4 +69,4 @@ encapsulating operations around such relationships — for example, updating the ### Kibana client code This layer deals almost exclusively with data in the form of client models. The one exception to this rule is when the client code needs -to bootstrap a model instance from a bare JS object — for example, creating a new `Watch` model from the contents of the Add/Edit Watch Form. +to bootstrap a model instance from a bare JS object — for example, creating a new `Watch` model from the contents of the Add/Edit Watch Form. \ No newline at end of file From d84ac13a4abd5de1f97aede2843b7c62745d191b Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Thu, 29 Oct 2020 14:04:37 -0400 Subject: [PATCH 41/73] [Logs App] Fix logs permissions for alert management (#81199) * Mimics metrics permissions for alert mgmt in logs feature * Updates logs security functional tests Alert management permissions were added for read and all logs users in this PR, so both of these users now have "Stack Management" appear in the nav. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/infra/server/features.ts | 9 +++++++++ .../apps/infra/feature_controls/logs_security.ts | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index d49755b6f68c1..c768cae247a99 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -68,6 +68,9 @@ export const LOGS_FEATURE = { category: DEFAULT_APP_CATEGORIES.observability, app: ['infra', 'logs', 'kibana'], catalogue: ['infralogging', 'logs'], + management: { + insightsAndAlerting: ['triggersActions'], + }, alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], privileges: { all: { @@ -81,6 +84,9 @@ export const LOGS_FEATURE = { alerting: { all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], }, + management: { + insightsAndAlerting: ['triggersActions'], + }, ui: ['show', 'configureSource', 'save'], }, read: { @@ -90,6 +96,9 @@ export const LOGS_FEATURE = { alerting: { read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], }, + management: { + insightsAndAlerting: ['triggersActions'], + }, savedObject: { all: [], read: ['infrastructure-ui-source'], diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index de6353d6b0456..78edbafa4ae47 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs']); + expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs']); + expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { From ee7ce25048818611416e9cc3a9791aa82d19583c Mon Sep 17 00:00:00 2001 From: Jane Miller <57721870+jmiller263@users.noreply.github.com> Date: Thu, 29 Oct 2020 14:09:01 -0400 Subject: [PATCH 42/73] [SECURITY_SOLUTION] 145: Advanced Policy UI (#80390) * Create Policies for each generated host * Refactor Ingest setup to also setup Fleet * Rename prop name * Add generic response type to KbnClient.request + support for headers * first attempt at adding fleet agent registration * a little closer with fleet integration * SUCCESS. Able to enroll agent and set it to online * update names to be policy * policy generator has advanced types in endpoint confit * linting * flesh out callback * add submit button for verify_peer * add verify hostname field * 145 generalize cb * 145 fix setAgain and getValue * 145 merge conflict * 145 add verify_hostname back, start loop for form * 145 remove OS trick * 145 make AdvancedPolicyForms its own component * 145 grid partially working * 145 back to basics * 145 back to basics * 145 rolled back grid * 145 flex table working * 145 undo accidental change * 145 remove extra schema file * 145 remove unused variable * 145 kevin's PR feedback * 145 fix type check and jest * 145 EuiFlexGroups * 145 use simple EuiFormRow and add show/hide buttons * 145 move all advanced policy code to advanced file; remove unnec test code * 145 fix IDs * 145 take out unnecessary stuff * 145 removed a couple more lines * 145 add some fields back in * 145 add spacer Co-authored-by: Paul Tavares Co-authored-by: Elastic Machine Co-authored-by: kevinlog Co-authored-by: Candace Park --- .../common/endpoint/types/index.ts | 9 +- .../policy/models/advanced_policy_schema.ts | 315 ++++++++++++++++++ .../policy/models/policy_details_config.ts | 41 +-- .../policy/store/policy_details/index.test.ts | 8 +- .../policy/store/policy_details/selectors.ts | 3 + .../management/pages/policy/view/index.ts | 1 + .../pages/policy/view/policy_advanced.tsx | 124 +++++++ .../pages/policy/view/policy_details.tsx | 18 + .../view/policy_forms/protections/malware.tsx | 8 +- 9 files changed, 477 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 882b3e5182bf3..79157018c315a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -860,6 +860,7 @@ type KbnConfigSchemaNonOptionalProps> = Pi */ export interface PolicyConfig { windows: { + advanced?: {}; events: { dll_and_driver_load: boolean; dns: boolean; @@ -881,6 +882,7 @@ export interface PolicyConfig { }; }; mac: { + advanced?: {}; events: { file: boolean; process: boolean; @@ -898,6 +900,7 @@ export interface PolicyConfig { }; }; linux: { + advanced?: {}; events: { file: boolean; process: boolean; @@ -916,15 +919,15 @@ export interface UIPolicyConfig { /** * Windows-specific policy configuration that is supported via the UI */ - windows: Pick; + windows: Pick; /** * Mac-specific policy configuration that is supported via the UI */ - mac: Pick; + mac: Pick; /** * Linux-specific policy configuration that is supported via the UI */ - linux: Pick; + linux: Pick; } /** Policy: Malware protection fields */ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts new file mode 100644 index 0000000000000..d25588dabedc6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface AdvancedPolicySchemaType { + key: string; + first_supported_version: string; + last_supported_version?: string; + documentation: string; +} + +export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ + { + key: 'linux.advanced.agent.connection_delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.manifest_relative_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.elasticsearch.delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.elasticsearch.tls.verify_peer', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'linux.advanced.elasticsearch.tls.verify_hostname', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'linux.advanced.elasticsearch.tls.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.agent.connection_delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.manifest_relative_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.elasticsearch.delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.elasticsearch.tls.verify_peer', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'mac.advanced.elasticsearch.tls.verify_hostname', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'mac.advanced.elasticsearch.tls.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.malware.quarantine', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.connect', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.harden', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.process', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.filewrite', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.network', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.agent.connection_delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.manifest_relative_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.elasticsearch.delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.elasticsearch.tls.verify_peer', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'windows.advanced.elasticsearch.tls.verify_hostname', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'windows.advanced.elasticsearch.tls.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.malware.quarantine', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.ransomware.mbr', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.ransomware.canary', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.connect', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.harden', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.process', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.filewrite', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.network', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.fileopen', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.asyncimageload', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.syncimageload', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.registry', + first_supported_version: '7.11', + documentation: '', + }, +]; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts index 4d32a9fbec694..8d5c19a9e489a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts @@ -4,46 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { cloneDeep } from 'lodash'; import { UIPolicyConfig } from '../../../../../common/endpoint/types'; -/** - * A typed Object.entries() function where the keys and values are typed based on the given object - */ -const entries = (o: T): Array<[keyof T, T[keyof T]]> => - Object.entries(o) as Array<[keyof T, T[keyof T]]>; -type DeepPartial = { [K in keyof T]?: DeepPartial }; - -/** - * Returns a deep copy of `UIPolicyConfig` object - */ -export function clone(policyDetailsConfig: UIPolicyConfig): UIPolicyConfig { - const clonedConfig: DeepPartial = {}; - for (const [key, val] of entries(policyDetailsConfig)) { - if (typeof val === 'object') { - const valClone: Partial = {}; - clonedConfig[key] = valClone; - for (const [key2, val2] of entries(val)) { - if (typeof val2 === 'object') { - valClone[key2] = { - ...val2, - }; - } else { - clonedConfig[key] = { - ...val, - }; - } - } - } else { - clonedConfig[key] = val; - } - } - - /** - * clonedConfig is typed as DeepPartial so we can construct the copy from an empty object - */ - return clonedConfig as UIPolicyConfig; -} - /** * Returns value from `configuration` */ @@ -69,7 +32,7 @@ export const setIn = (a: UIPolicyConfig) => (k >( v: V ): UIPolicyConfig => { - const c = clone(a); + const c = cloneDeep(a); c[key][subKey][leafKey] = v; return c; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index b76e0c8acf4c3..89ba05547f447 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -8,7 +8,6 @@ import { PolicyDetailsState } from '../../types'; import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction, policyDetailsMiddlewareFactory } from './index'; import { policyConfig } from './selectors'; -import { clone } from '../../models/policy_details_config'; import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; import { PolicyData } from '../../../../../../common/endpoint/types'; import { @@ -20,6 +19,7 @@ import { createAppRootMockRenderer, } from '../../../../../common/mock/endpoint'; import { HttpFetchOptions } from 'kibana/public'; +import { cloneDeep } from 'lodash'; describe('policy details: ', () => { let store: Store; @@ -93,7 +93,7 @@ describe('policy details: ', () => { throw new Error(); } - const newPayload1 = clone(config); + const newPayload1 = cloneDeep(config); newPayload1.windows.events.process = true; dispatch({ @@ -115,7 +115,7 @@ describe('policy details: ', () => { throw new Error(); } - const newPayload1 = clone(config); + const newPayload1 = cloneDeep(config); newPayload1.mac.events.file = true; dispatch({ @@ -137,7 +137,7 @@ describe('policy details: ', () => { throw new Error(); } - const newPayload1 = clone(config); + const newPayload1 = cloneDeep(config); newPayload1.linux.events.file = true; dispatch({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts index 953438526b87e..f275124a73527 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts @@ -103,16 +103,19 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel (windows, mac, linux) => { return { windows: { + advanced: windows.advanced, events: windows.events, malware: windows.malware, popup: windows.popup, }, mac: { + advanced: mac.advanced, events: mac.events, malware: mac.malware, popup: mac.popup, }, linux: { + advanced: linux.advanced, events: linux.events, }, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts index 9c227ca81a426..ce942205b1620 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts @@ -6,3 +6,4 @@ export * from './policy_list'; export * from './policy_details'; +export * from './policy_advanced'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx new file mode 100644 index 0000000000000..b4b82b7f692b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { EuiFieldText, EuiFormRow, EuiPanel, EuiText } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { policyConfig } from '../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from './policy_hooks'; +import { AdvancedPolicySchema } from '../models/advanced_policy_schema'; + +function setValue(obj: Record, value: string, path: string[]) { + let newPolicyConfig = obj; + for (let i = 0; i < path.length - 1; i++) { + if (!newPolicyConfig[path[i]]) { + newPolicyConfig[path[i]] = {} as Record; + } + newPolicyConfig = newPolicyConfig[path[i]] as Record; + } + newPolicyConfig[path[path.length - 1]] = value; +} + +function getValue(obj: Record, path: string[]) { + let currentPolicyConfig = obj; + + for (let i = 0; i < path.length - 1; i++) { + if (currentPolicyConfig[path[i]]) { + currentPolicyConfig = currentPolicyConfig[path[i]] as Record; + } else { + return undefined; + } + } + return currentPolicyConfig[path[path.length - 1]]; +} + +export const AdvancedPolicyForms = React.memo(() => { + return ( + <> + +

+ +

+
+ + {AdvancedPolicySchema.map((advancedField, index) => { + const configPath = advancedField.key.split('.'); + return ( + + ); + })} + + + ); +}); + +AdvancedPolicyForms.displayName = 'AdvancedPolicyForms'; + +const PolicyAdvanced = React.memo( + ({ + configPath, + firstSupportedVersion, + lastSupportedVersion, + }: { + configPath: string[]; + firstSupportedVersion: string; + lastSupportedVersion?: string; + }) => { + const dispatch = useDispatch(); + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const onChange = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + setValue( + (newPayload as unknown) as Record, + event.target.value, + configPath + ); + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [dispatch, policyDetailsConfig, configPath] + ); + + const value = + policyDetailsConfig && + getValue((policyDetailsConfig as unknown) as Record, configPath); + + return ( + <> + + {lastSupportedVersion + ? `${firstSupportedVersion}-${lastSupportedVersion}` + : `${firstSupportedVersion}+`} + + } + > + + + + ); + } +); + +PolicyAdvanced.displayName = 'PolicyAdvanced'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 40c982cfc071b..8fc5de48f36db 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -47,6 +47,7 @@ import { MANAGEMENT_APP_ID } from '../../../common/constants'; import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; import { WrapperPage } from '../../../../common/components/wrapper_page'; import { HeaderPage } from '../../../../common/components/header_page'; +import { AdvancedPolicyForms } from './policy_advanced'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -69,6 +70,7 @@ export const PolicyDetails = React.memo(() => { // Local state const [showConfirm, setShowConfirm] = useState(false); const [routeState, setRouteState] = useState(); + const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false); const policyName = policyItem?.name ?? ''; const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); @@ -128,6 +130,10 @@ export const PolicyDetails = React.memo(() => { setShowConfirm(false); }, []); + const handleAdvancedPolicyClick = useCallback(() => { + setShowAdvancedPolicy(!showAdvancedPolicy); + }, [showAdvancedPolicy]); + useEffect(() => { if (!routeState && locationRouteState) { setRouteState(locationRouteState); @@ -245,6 +251,18 @@ export const PolicyDetails = React.memo(() => { + + + + + + + + {showAdvancedPolicy && } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 6773ed6541927..215851fb28152 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; @@ -27,7 +28,6 @@ import { OS, MalwareProtectionOSes } from '../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { clone } from '../../../models/policy_details_config'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; import { popupVersionsMap } from './popup_options_to_versions'; @@ -50,7 +50,7 @@ const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: const handleRadioChange = useCallback(() => { if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); + const newPayload = cloneDeep(policyDetailsConfig); for (const os of OSes) { newPayload[os][protection].mode = id; if (id === ProtectionModes.prevent) { @@ -141,7 +141,7 @@ export const MalwareProtections = React.memo(() => { const handleSwitchChange = useCallback( (event) => { if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); + const newPayload = cloneDeep(policyDetailsConfig); if (event.target.checked === false) { for (const os of OSes) { newPayload[os][protection].mode = ProtectionModes.off; @@ -165,7 +165,7 @@ export const MalwareProtections = React.memo(() => { const handleUserNotificationCheckbox = useCallback( (event) => { if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); + const newPayload = cloneDeep(policyDetailsConfig); for (const os of OSes) { newPayload[os].popup[protection].enabled = event.target.checked; } From 5752b7a8ddd28d245bd5f2887b1d146fe99936aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2020 14:21:36 -0400 Subject: [PATCH 43/73] Bump xml-crypto from 1.4.0 to 2.0.0 (#81859) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index ec4388c0b8b7d..ba464a21263d7 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -259,7 +259,7 @@ "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", "whatwg-fetch": "^3.0.0", - "xml-crypto": "^1.4.0", + "xml-crypto": "^2.0.0", "yargs": "^15.4.1" }, "dependencies": { diff --git a/yarn.lock b/yarn.lock index b2216537bbd7c..6b77b14f09d42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29247,10 +29247,10 @@ xhr@^2.0.1: parse-headers "^2.0.0" xtend "^4.0.0" -xml-crypto@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-1.4.0.tgz#de1cec8cd31cbd689cd90d3d6e8a27d4ae807de7" - integrity sha512-K8FRdRxICVulK4WhiTUcJrRyAIJFPVOqxfurA3x/JlmXBTxy+SkEENF6GeRt7p/rB6WSOUS9g0gXNQw5n+407g== +xml-crypto@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-2.0.0.tgz#54cd268ad9d31930afcf7092cbb664258ca9e826" + integrity sha512-/a04qr7RpONRZHOxROZ6iIHItdsQQjN3sj8lJkYDDss8tAkEaAs0VrFjb3tlhmS5snQru5lTs9/5ISSMdPDHlg== dependencies: xmldom "0.1.27" xpath "0.0.27" From e0b6b0ba5cabe78fb884e629144e4d088bdf92c6 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Thu, 29 Oct 2020 13:34:41 -0500 Subject: [PATCH 44/73] Vislib visualization renderer (#80744) --- .../public/components/tag_cloud_chart.tsx | 4 +- .../public/timelion_vis_renderer.tsx | 6 - .../public/__snapshots__/pie_fn.test.ts.snap | 5 +- .../public/__snapshots__/to_ast.test.ts.snap | 22 + .../__snapshots__/to_ast_pie.test.ts.snap | 19 + src/plugins/vis_type_vislib/public/area.ts | 19 +- .../public/components/options/gauge/index.tsx | 4 +- .../components/options/heatmap/index.tsx | 4 +- .../public/components/options/index.tsx | 51 + .../options/metrics_axes/index.test.tsx | 2 +- .../components/options/metrics_axes/index.tsx | 4 +- .../public/components/options/pie.tsx | 4 +- .../components/options/point_series/index.ts | 4 +- src/plugins/vis_type_vislib/public/gauge.ts | 11 +- src/plugins/vis_type_vislib/public/goal.ts | 11 +- src/plugins/vis_type_vislib/public/heatmap.ts | 20 +- .../vis_type_vislib/public/histogram.ts | 19 +- .../vis_type_vislib/public/horizontal_bar.ts | 19 +- src/plugins/vis_type_vislib/public/index.scss | 2 +- src/plugins/vis_type_vislib/public/line.ts | 19 +- src/plugins/vis_type_vislib/public/pie.ts | 16 +- src/plugins/vis_type_vislib/public/pie_fn.ts | 37 +- src/plugins/vis_type_vislib/public/plugin.ts | 73 +- .../public/sample_vis.test.mocks.ts | 3307 +++++++++++++++++ .../vis_type_vislib/public/services.ts | 5 - .../vis_type_vislib/public/to_ast.test.ts | 60 + src/plugins/vis_type_vislib/public/to_ast.ts | 103 + .../options/index.ts => to_ast_esaggs.ts} | 26 +- .../vis_type_vislib/public/to_ast_pie.test.ts | 60 + .../vis_type_vislib/public/to_ast_pie.ts | 50 + src/plugins/vis_type_vislib/public/types.ts | 6 + .../vis_type_vislib/public/vis_controller.tsx | 172 +- .../vis_type_vislib/public/vis_renderer.tsx | 61 + .../public/vis_type_vislib_vis_fn.ts | 42 +- .../public/vis_type_vislib_vis_types.ts | 27 +- .../vis_type_vislib/public/vis_wrapper.tsx | 89 + .../public/vislib/_vislib_vis_type.scss | 14 +- .../vislib/components/legend/_legend.scss | 9 +- .../vislib/components/legend/legend.test.tsx | 36 +- .../vislib/components/legend/legend.tsx | 49 +- .../helpers/point_series/_init_x_axis.ts | 4 +- .../helpers/point_series/point_series.ts | 5 +- .../public/vislib/lib/_handler.scss | 17 - .../public/vislib/lib/_index.scss | 1 - .../public/vislib/lib/dispatch.js | 4 - .../public/vislib/lib/handler.js | 22 +- .../public/vislib/lib/handler.test.js | 2 +- .../public/vislib/lib/vis_config.js | 1 - .../vis_type_vislib/public/vislib/vis.js | 13 +- .../public/vislib/visualizations/_chart.js | 4 +- .../vislib/visualizations/_vis_fixture.js | 17 +- .../vislib/visualizations/gauge_chart.test.js | 4 +- .../public/vislib/visualizations/pie_chart.js | 4 +- .../vislib/visualizations/pie_chart.test.js | 8 +- .../vislib/visualizations/point_series.js | 14 +- .../visualizations/point_series/area_chart.js | 4 +- .../point_series/area_chart.test.js | 4 +- .../point_series/column_chart.js | 4 +- .../point_series/column_chart.test.js | 16 +- .../point_series/heatmap_chart.test.js | 4 +- .../visualizations/point_series/line_chart.js | 4 +- .../point_series/line_chart.test.js | 4 +- .../public/components/_visualization.scss | 4 +- src/plugins/visualizations/public/index.ts | 1 + .../__snapshots__/build_pipeline.test.ts.snap | 2 - .../public/legacy/build_pipeline.test.ts | 164 - .../public/legacy/build_pipeline.ts | 96 - src/plugins/visualizations/public/types.ts | 2 +- .../page_objects/visualize_chart_page.ts | 2 +- 69 files changed, 4235 insertions(+), 687 deletions(-) create mode 100644 src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap create mode 100644 src/plugins/vis_type_vislib/public/__snapshots__/to_ast_pie.test.ts.snap create mode 100644 src/plugins/vis_type_vislib/public/components/options/index.tsx create mode 100644 src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts create mode 100644 src/plugins/vis_type_vislib/public/to_ast.test.ts create mode 100644 src/plugins/vis_type_vislib/public/to_ast.ts rename src/plugins/vis_type_vislib/public/{components/options/index.ts => to_ast_esaggs.ts} (51%) create mode 100644 src/plugins/vis_type_vislib/public/to_ast_pie.test.ts create mode 100644 src/plugins/vis_type_vislib/public/to_ast_pie.ts create mode 100644 src/plugins/vis_type_vislib/public/vis_renderer.tsx create mode 100644 src/plugins/vis_type_vislib/public/vis_wrapper.tsx delete mode 100644 src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx index cb0daa6d29382..a14328ac994f0 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx @@ -45,7 +45,9 @@ export const TagCloudChart = ({ const visController = useRef(null); useEffect(() => { - visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent); + if (chartDiv.current) { + visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent); + } return () => { visController.current.destroy(); visController.current = null; diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index 04579407105e8..2c914d3c5b662 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -42,12 +42,6 @@ export const getTimelionVisRenderer: ( const [seriesList] = visData.sheet; const showNoResult = !seriesList || !seriesList.list.length; - if (showNoResult) { - // send the render complete event when there is no data to show - // to notify that a chart is updated - handlers.done(); - } - render( diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap index 2cee55e4751c2..b64366c1ce0f3 100644 --- a/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap +++ b/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap @@ -2,12 +2,9 @@ exports[`interpreter/functions#pie returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "vislib_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "addLegend": true, "addTooltip": true, diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..c3ffc0dd08412 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vislib vis toExpressionAst function should match basic snapshot 1`] = ` +Object { + "addArgument": [Function], + "arguments": Object { + "type": Array [ + "area", + ], + "visConfig": Array [ + "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + ], + }, + "getArgument": [Function], + "name": "vislib_vis", + "removeArgument": [Function], + "replaceArgument": [Function], + "toAst": [Function], + "toString": [Function], + "type": "expression_function_builder", +} +`; diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast_pie.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast_pie.test.ts.snap new file mode 100644 index 0000000000000..b8dc4b31747c4 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast_pie.test.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vislib pie vis toExpressionAst function should match basic snapshot 1`] = ` +Object { + "addArgument": [Function], + "arguments": Object { + "visConfig": Array [ + "{\\"type\\":\\"pie\\",\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"right\\",\\"isDonut\\":true,\\"labels\\":{\\"show\\":true,\\"values\\":true,\\"last_level\\":true,\\"truncate\\":100},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{}},\\"buckets\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + ], + }, + "getArgument": [Function], + "name": "vislib_pie_vis", + "removeArgument": [Function], + "replaceArgument": [Function], + "toAst": [Function], + "toString": [Function], + "type": "expression_function_builder", +} +`; diff --git a/src/plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts index ec90fbd1746a1..531958d6b3db3 100644 --- a/src/plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -37,22 +37,20 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { BasicVislibParams } from './types'; -export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const areaVisTypeDefinition: BaseVisTypeOptions = { name: 'area', title: i18n.translate('visTypeVislib.area.areaTitle', { defaultMessage: 'Area' }), icon: 'visArea', description: i18n.translate('visTypeVislib.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'area', @@ -131,9 +129,6 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => labels: {}, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -190,4 +185,4 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx index 6109b548f9412..911ee293f580e 100644 --- a/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx @@ -60,4 +60,6 @@ function GaugeOptions(props: VisOptionsProps) { ); } -export { GaugeOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { GaugeOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx index 7a89496d9441e..312cf60fda6b0 100644 --- a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx @@ -185,4 +185,6 @@ function HeatmapOptions(props: VisOptionsProps) { ); } -export { HeatmapOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { HeatmapOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/index.tsx b/src/plugins/vis_type_vislib/public/components/options/index.tsx new file mode 100644 index 0000000000000..18c41bf289b11 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/index.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { ValidationVisOptionsProps } from '../common'; +import { GaugeVisParams } from '../../gauge'; +import { PieVisParams } from '../../pie'; +import { BasicVislibParams } from '../../types'; +import { HeatmapVisParams } from '../../heatmap'; + +const GaugeOptionsLazy = lazy(() => import('./gauge')); +const PieOptionsLazy = lazy(() => import('./pie')); +const PointSeriesOptionsLazy = lazy(() => import('./point_series')); +const HeatmapOptionsLazy = lazy(() => import('./heatmap')); +const MetricsAxisOptionsLazy = lazy(() => import('./metrics_axes')); + +export const GaugeOptions = (props: VisOptionsProps) => ( + +); + +export const PieOptions = (props: VisOptionsProps) => ; + +export const PointSeriesOptions = (props: ValidationVisOptionsProps) => ( + +); + +export const HeatmapOptions = (props: VisOptionsProps) => ( + +); + +export const MetricsAxisOptions = (props: ValidationVisOptionsProps) => ( + +); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx index 0cc737f19e5c6..63881fea1ad88 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { IAggConfig, IAggType } from 'src/plugins/data/public'; -import { MetricsAxisOptions } from './index'; +import MetricsAxisOptions from './index'; import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; import { ValidationVisOptionsProps } from '../../common'; import { Positions } from '../../../utils/collections'; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index 18687404b9114..0862c47c35cff 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -325,4 +325,6 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ) : null; } -export { MetricsAxisOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { MetricsAxisOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/pie.tsx b/src/plugins/vis_type_vislib/public/components/options/pie.tsx index 54ba307982967..30828bfc6a3ea 100644 --- a/src/plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/pie.tsx @@ -99,4 +99,6 @@ function PieOptions(props: VisOptionsProps) { ); } -export { PieOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { PieOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts b/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts index fb94ec6743faa..937b92c950430 100644 --- a/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export { PointSeriesOptions } from './point_series'; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { PointSeriesOptions as default } from './point_series'; diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index 561c45d26fa7f..86e3b8793d618 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -24,8 +24,9 @@ import { AggGroupNames } from '../../data/public'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, GaugeTypes } from './utils/collections'; import { ColorModes, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../charts/public'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; +import { toExpressionAst } from './to_ast'; +import { BaseVisTypeOptions } from '../../visualizations/public'; +import { BasicVislibParams } from './types'; export interface Gauge extends ColorSchemaParams { backStyle: 'Full'; @@ -55,7 +56,7 @@ export interface GaugeVisParams { gauge: Gauge; } -export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const gaugeVisTypeDefinition: BaseVisTypeOptions = { name: 'gauge', title: i18n.translate('visTypeVislib.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), icon: 'visGauge', @@ -63,6 +64,7 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: "Gauges indicate the status of a metric. Use it to show how a metric's value relates to reference threshold values.", }), + toExpressionAst, visConfig: { defaults: { type: 'gauge', @@ -109,7 +111,6 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, }, }, - visualization: createVislibVisController(deps), editorConfig: { collections: getGaugeCollections(), optionsTemplate: GaugeOptions, @@ -145,4 +146,4 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => ]), }, useCustomNoDataScreen: true, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index 5f74698938a0b..32574fb5b0a9c 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -21,20 +21,21 @@ import { i18n } from '@kbn/i18n'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, GaugeTypes } from './utils/collections'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { ColorModes, ColorSchemas } from '../../charts/public'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; +import { toExpressionAst } from './to_ast'; +import { BaseVisTypeOptions } from '../../visualizations/public'; +import { BasicVislibParams } from './types'; -export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const goalVisTypeDefinition: BaseVisTypeOptions = { name: 'goal', title: i18n.translate('visTypeVislib.goal.goalTitle', { defaultMessage: 'Goal' }), icon: 'visGoal', description: i18n.translate('visTypeVislib.goal.goalDescription', { defaultMessage: 'A goal chart indicates how close you are to your final goal.', }), - visualization: createVislibVisController(deps), + toExpressionAst, visConfig: { defaults: { addTooltip: true, @@ -110,4 +111,4 @@ export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ]), }, useCustomNoDataScreen: true, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index bd3d02029cb23..f970eddd645f5 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -23,12 +23,11 @@ import { RangeValues, Schemas } from '../../vis_default_editor/public'; import { AggGroupNames } from '../../data/public'; import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; import { HeatmapOptions } from './components/options'; -import { createVislibVisController } from './vis_controller'; import { TimeMarker } from './vislib/visualizations/time_marker'; -import { CommonVislibParams, ValueAxis } from './types'; -import { VisTypeVislibDependencies } from './plugin'; +import { BasicVislibParams, CommonVislibParams, ValueAxis } from './types'; import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; @@ -42,17 +41,15 @@ export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams times: TimeMarker[]; } -export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const heatmapVisTypeDefinition: BaseVisTypeOptions = { name: 'heatmap', title: i18n.translate('visTypeVislib.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), icon: 'heatmap', description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; - }, - visualization: createVislibVisController(deps), + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], + toExpressionAst, visConfig: { defaults: { type: 'heatmap', @@ -86,9 +83,6 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) ], }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getHeatmapCollections(), optionsTemplate: HeatmapOptions, @@ -142,4 +136,4 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts index 8aeeb4ec533ab..d5fb92f5c6a0c 100644 --- a/src/plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -36,12 +36,12 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BasicVislibParams } from './types'; +import { toExpressionAst } from './to_ast'; -export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const histogramVisTypeDefinition: BaseVisTypeOptions = { name: 'histogram', title: i18n.translate('visTypeVislib.histogram.histogramTitle', { defaultMessage: 'Vertical Bar', @@ -50,10 +50,8 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies description: i18n.translate('visTypeVislib.histogram.histogramDescription', { defaultMessage: 'Assign a continuous variable to each axis', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'histogram', @@ -133,9 +131,6 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies }, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -192,4 +187,4 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts index 702581828e60d..f1a5365e5ae74 100644 --- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -34,12 +34,12 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BasicVislibParams } from './types'; +import { toExpressionAst } from './to_ast'; -export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const horizontalBarVisTypeDefinition: BaseVisTypeOptions = { name: 'horizontal_bar', title: i18n.translate('visTypeVislib.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal Bar', @@ -48,10 +48,8 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen description: i18n.translate('visTypeVislib.horizontalBar.horizontalBarDescription', { defaultMessage: 'Assign a continuous variable to each axis', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'histogram', @@ -130,9 +128,6 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen }, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -189,4 +184,4 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/index.scss b/src/plugins/vis_type_vislib/public/index.scss index 64445648ba84a..3c347aebde225 100644 --- a/src/plugins/vis_type_vislib/public/index.scss +++ b/src/plugins/vis_type_vislib/public/index.scss @@ -1 +1 @@ -@import './vislib/index' +@import './vislib/index'; diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts index 6e9190229114b..a65b0bcf7e2bb 100644 --- a/src/plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -35,22 +35,20 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { BasicVislibParams } from './types'; -export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const lineVisTypeDefinition: BaseVisTypeOptions = { name: 'line', title: i18n.translate('visTypeVislib.line.lineTitle', { defaultMessage: 'Line' }), icon: 'visLine', description: i18n.translate('visTypeVislib.line.lineDescription', { defaultMessage: 'Emphasize trends', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'line', @@ -129,9 +127,6 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -182,4 +177,4 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index 1e81dbdde3f68..58f7dd0df89e8 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -23,14 +23,12 @@ import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; -import { createVislibVisController } from './vis_controller'; import { CommonVislibParams } from './types'; -import { VisTypeVislibDependencies } from './plugin'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast_pie'; export interface PieVisParams extends CommonVislibParams { type: 'pie'; - addLegend: boolean; isDonut: boolean; labels: { show: boolean; @@ -40,17 +38,15 @@ export interface PieVisParams extends CommonVislibParams { }; } -export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const pieVisTypeDefinition: BaseVisTypeOptions = { name: 'pie', title: i18n.translate('visTypeVislib.pie.pieTitle', { defaultMessage: 'Pie' }), icon: 'visPie', description: i18n.translate('visTypeVislib.pie.pieDescription', { defaultMessage: 'Compare parts of a whole', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], + toExpressionAst, visConfig: { defaults: { type: 'pie', @@ -108,4 +104,4 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ( }, hierarchicalData: true, responseHandler: 'vislib_slices', -}); +}; diff --git a/src/plugins/vis_type_vislib/public/pie_fn.ts b/src/plugins/vis_type_vislib/public/pie_fn.ts index bee200cbe30ee..c9da9e9bd9fab 100644 --- a/src/plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.ts @@ -18,27 +18,35 @@ */ import { i18n } from '@kbn/i18n'; + import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; + // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; +import { PieVisParams } from './pie'; +import { vislibVisName } from './vis_type_vislib_vis_fn'; + +export const vislibPieName = 'vislib_pie_vis'; interface Arguments { visConfig: string; } -type VisParams = Required; - interface RenderValue { - visConfig: VisParams; + visData: unknown; + visType: string; + visConfig: PieVisParams; } -export const createPieVisFn = (): ExpressionFunctionDefinition< - 'kibana_pie', +export type VisTypeVislibPieExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof vislibPieName, Datatable, Arguments, Render -> => ({ - name: 'kibana_pie', +>; + +export const createPieVisFn = (): VisTypeVislibPieExpressionFunctionDefinition => ({ + name: vislibPieName, type: 'render', inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.pie.help', { @@ -48,23 +56,20 @@ export const createPieVisFn = (): ExpressionFunctionDefinition< visConfig: { types: ['string'], default: '"{}"', - help: '', + help: 'vislib pie vis config', }, }, fn(input, args) { - const visConfig = JSON.parse(args.visConfig); - const convertedData = vislibSlicesResponseHandler(input, visConfig.dimensions); + const visConfig = JSON.parse(args.visConfig) as PieVisParams; + const visData = vislibSlicesResponseHandler(input, visConfig.dimensions); return { type: 'render', - as: 'visualization', + as: vislibVisName, value: { - visData: convertedData, - visType: 'pie', + visData, visConfig, - params: { - listenOnChange: true, - }, + visType: 'pie', }, }; }, diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index c6a6b6f82592b..f183042fd5201 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -17,40 +17,20 @@ * under the License. */ -import './index.scss'; - -import { - CoreSetup, - CoreStart, - Plugin, - IUiSettingsClient, - PluginInitializerContext, -} from 'kibana/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { BaseVisTypeOptions, VisualizationsSetup } from '../../visualizations/public'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; import { createPieVisFn } from './pie_fn'; -import { - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, -} from './vis_type_vislib_vis_types'; +import { visLibVisTypeDefinitions, pieVisTypeDefinition } from './vis_type_vislib_vis_types'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService, setDataActions, setKibanaLegacy } from './services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; - -export interface VisTypeVislibDependencies { - uiSettings: IUiSettingsClient; - charts: ChartsPluginSetup; -} +import { setFormatService, setDataActions } from './services'; +import { getVislibVisRenderer } from './vis_renderer'; +import { BasicVislibParams } from './types'; /** @internal */ export interface VisTypeVislibPluginSetupDependencies { @@ -66,54 +46,37 @@ export interface VisTypeVislibPluginStartDependencies { kibanaLegacy: KibanaLegacyStart; } -type VisTypeVislibCoreSetup = CoreSetup; +export type VisTypeVislibCoreSetup = CoreSetup; /** @internal */ -export class VisTypeVislibPlugin implements Plugin { +export class VisTypeVislibPlugin + implements + Plugin { constructor(public initializerContext: PluginInitializerContext) {} public async setup( core: VisTypeVislibCoreSetup, { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies ) { - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - charts, - }; - const vislibTypes = [ - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, - ]; - const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; - // if visTypeXy plugin is disabled it's config will be undefined if (!visTypeXy) { - const convertedTypes: any[] = []; + const convertedTypes: Array> = []; const convertedFns: any[] = []; // Register legacy vislib types that have been converted convertedFns.forEach(expressions.registerFunction); - convertedTypes.forEach((vis) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); + convertedTypes.forEach(visualizations.createBaseVisualization); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); } - // Register non-converted types - vislibFns.forEach(expressions.registerFunction); - vislibTypes.forEach((vis) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); + visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); + visualizations.createBaseVisualization(pieVisTypeDefinition); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); + [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); } - public start(core: CoreStart, { data, kibanaLegacy }: VisTypeVislibPluginStartDependencies) { + public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { setFormatService(data.fieldFormats); setDataActions(data.actions); - setKibanaLegacy(kibanaLegacy); } } diff --git a/src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts new file mode 100644 index 0000000000000..324e8e00f37fc --- /dev/null +++ b/src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts @@ -0,0 +1,3307 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const samplePieVis = { + type: { + name: 'pie', + title: 'Pie', + description: 'Compare parts of a whole', + icon: 'visPie', + stage: 'production', + options: { + showTimePicker: true, + showQueryBar: true, + showFilterBar: true, + showIndexSelection: true, + hierarchicalData: false, + }, + visConfig: { + defaults: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }, + editorConfig: { + collections: { + legendPositions: [ + { + text: 'Top', + value: 'top', + }, + { + text: 'Left', + value: 'left', + }, + { + text: 'Right', + value: 'right', + }, + { + text: 'Bottom', + value: 'bottom', + }, + ], + }, + schemas: { + all: [ + { + group: 'metrics', + name: 'metric', + title: 'Slice size', + min: 1, + max: 1, + aggFilter: ['sum', 'count', 'cardinality', 'top_hits'], + defaults: [ + { + schema: 'metric', + type: 'count', + }, + ], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'segment', + title: 'Split slices', + min: 0, + max: null, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'split', + title: 'Split chart', + mustBeFirst: true, + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + params: [ + { + name: 'row', + default: true, + }, + ], + editor: false, + }, + ], + buckets: [null, null], + metrics: [null], + }, + }, + hidden: false, + requestHandler: 'courier', + responseHandler: 'vislib_slices', + hierarchicalData: true, + useCustomNoDataScreen: false, + }, + title: '[Flights] Airline Carrier', + description: '', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: true, + values: true, + last_level: true, + truncate: 100, + }, + }, + sessionState: {}, + data: { + searchSource: { + id: 'data_source1', + requestStartHandlers: [], + inheritOptions: {}, + history: [], + fields: { + filter: [], + query: { + query: '', + language: 'kuery', + }, + index: { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + title: 'kibana_sample_data_flights', + fieldFormatMap: { + AvgTicketPrice: { + id: 'number', + params: { + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + pattern: '$0,0.[00]', + }, + }, + hour_of_day: { + id: 'number', + params: { + pattern: '00', + }, + }, + }, + fields: [ + { + count: 0, + name: 'AvgTicketPrice', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'Cancelled', + type: 'boolean', + esTypes: ['boolean'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'Carrier', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'Dest', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DestAirportID', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DestCityName', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DestCountry', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DestLocation', + type: 'geo_point', + esTypes: ['geo_point'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DestRegion', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DestWeather', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DistanceKilometers', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'DistanceMiles', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'FlightDelay', + type: 'boolean', + esTypes: ['boolean'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'FlightDelayMin', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'FlightDelayType', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'FlightNum', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'FlightTimeHour', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'FlightTimeMin', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'Origin', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'OriginAirportID', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'OriginCityName', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'OriginCountry', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'OriginLocation', + type: 'geo_point', + esTypes: ['geo_point'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'OriginRegion', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'OriginWeather', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: '_id', + type: 'string', + esTypes: ['_id'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: '_index', + type: 'string', + esTypes: ['_index'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: '_score', + type: 'number', + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_source', + type: '_source', + esTypes: ['_source'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_type', + type: 'string', + esTypes: ['_type'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: 'dayOfWeek', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'timestamp', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + script: "doc['timestamp'].value.hourOfDay", + lang: 'painless', + name: 'hour_of_day', + type: 'number', + scripted: true, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + ], + timeFieldName: 'timestamp', + metaFields: ['_source', '_id', '_type', '_index', '_score'], + version: 'WzM1LDFd', + originalSavedObjectBody: { + title: 'kibana_sample_data_flights', + timeFieldName: 'timestamp', + fields: + '[{"count":0,"name":"AvgTicketPrice","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Cancelled","type":"boolean","esTypes":["boolean"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Carrier","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Dest","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestAirportID","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestCityName","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestCountry","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestLocation","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestRegion","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestWeather","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DistanceKilometers","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DistanceMiles","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelay","type":"boolean","esTypes":["boolean"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelayMin","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelayType","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightNum","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightTimeHour","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightTimeMin","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Origin","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginAirportID","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginCityName","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginCountry","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginLocation","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginRegion","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginWeather","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"_id","type":"string","esTypes":["_id"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_index","type":"string","esTypes":["_index"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_score","type":"number","scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_source","type":"_source","esTypes":["_source"],"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_type","type":"string","esTypes":["_type"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"dayOfWeek","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"timestamp","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","name":"hour_of_day","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false}]', + fieldFormatMap: + '{"AvgTicketPrice":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5801","pathname":"/app/visualize","basePath":""},"pattern":"$0,0.[00]"}},"hour_of_day":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5801","pathname":"/app/visualize","basePath":""},"pattern":"00"}}}', + }, + shortDotsEnable: false, + fieldFormats: { + fieldFormats: {}, + defaultMap: { + ip: { + id: 'ip', + params: {}, + }, + date: { + id: 'date', + params: {}, + }, + date_nanos: { + id: 'date_nanos', + params: {}, + es: true, + }, + number: { + id: 'number', + params: {}, + }, + boolean: { + id: 'boolean', + params: {}, + }, + _source: { + id: '_source', + params: {}, + }, + _default_: { + id: 'string', + params: {}, + }, + }, + metaParamsOptions: {}, + }, + }, + }, + dependencies: { + legacy: { + loadingCount$: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + destination: { + closed: true, + }, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 13, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 3, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + }, + }, + aggs: { + typesRegistry: {}, + getResponseAggs: () => [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + toSerializedFieldFormat: () => ({ + id: 'number', + }), + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'Carrier', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + toSerializedFieldFormat: () => ({ + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }), + }, + ], + }, + }, + isHierarchical: () => true, + uiState: { + vis: { + legendOpen: false, + }, + }, +}; + +export const sampleAreaVis = { + type: { + name: 'area', + title: 'Area', + description: 'Emphasize the quantity beneath a line chart', + icon: 'visArea', + stage: 'production', + options: { + showTimePicker: true, + showQueryBar: true, + showFilterBar: true, + showIndexSelection: true, + hierarchicalData: false, + }, + visConfig: { + defaults: { + type: 'area', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'area', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + }, + }, + editorConfig: { + collections: { + legendPositions: [ + { + text: 'Top', + value: 'top', + }, + { + text: 'Left', + value: 'left', + }, + { + text: 'Right', + value: 'right', + }, + { + text: 'Bottom', + value: 'bottom', + }, + ], + positions: [ + { + text: 'Top', + value: 'top', + }, + { + text: 'Left', + value: 'left', + }, + { + text: 'Right', + value: 'right', + }, + { + text: 'Bottom', + value: 'bottom', + }, + ], + chartTypes: [ + { + text: 'Line', + value: 'line', + }, + { + text: 'Area', + value: 'area', + }, + { + text: 'Bar', + value: 'histogram', + }, + ], + axisModes: [ + { + text: 'Normal', + value: 'normal', + }, + { + text: 'Percentage', + value: 'percentage', + }, + { + text: 'Wiggle', + value: 'wiggle', + }, + { + text: 'Silhouette', + value: 'silhouette', + }, + ], + scaleTypes: [ + { + text: 'Linear', + value: 'linear', + }, + { + text: 'Log', + value: 'log', + }, + { + text: 'Square root', + value: 'square root', + }, + ], + chartModes: [ + { + text: 'Normal', + value: 'normal', + }, + { + text: 'Stacked', + value: 'stacked', + }, + ], + interpolationModes: [ + { + text: 'Straight', + value: 'linear', + }, + { + text: 'Smoothed', + value: 'cardinal', + }, + { + text: 'Stepped', + value: 'step-after', + }, + ], + thresholdLineStyles: [ + { + value: 'full', + text: 'Full', + }, + { + value: 'dashed', + text: 'Dashed', + }, + { + value: 'dot-dashed', + text: 'Dot-dashed', + }, + ], + }, + optionTabs: [ + { + name: 'advanced', + title: 'Metrics & axes', + }, + { + name: 'options', + title: 'Panel settings', + }, + ], + schemas: { + all: [ + { + group: 'metrics', + name: 'metric', + title: 'Y-axis', + aggFilter: ['!geo_centroid', '!geo_bounds'], + min: 1, + defaults: [ + { + schema: 'metric', + type: 'count', + }, + ], + max: null, + editor: false, + params: [], + }, + { + group: 'metrics', + name: 'radius', + title: 'Dot size', + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'segment', + title: 'X-axis', + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'group', + title: 'Split series', + min: 0, + max: 3, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'split', + title: 'Split chart', + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + params: [ + { + name: 'row', + default: true, + }, + ], + editor: false, + }, + ], + buckets: [null, null, null], + metrics: [null, null], + }, + }, + hidden: false, + requestHandler: 'courier', + responseHandler: 'none', + hierarchicalData: false, + useCustomNoDataScreen: false, + }, + title: '[eCommerce] Sales by Category', + description: '', + params: { + type: 'area', + grid: { + categoryLines: false, + style: { + color: '#eee', + }, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Sum of total_quantity', + }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { + label: 'Sum of total_quantity', + id: '1', + }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'top', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + dimensions: { + x: { + accessor: 0, + format: { + id: 'date', + params: { + pattern: 'YYYY-MM-DD HH:mm', + }, + }, + params: { + date: true, + interval: 43200000, + format: 'YYYY-MM-DD HH:mm', + bounds: { + min: '2020-09-30T12:41:13.795Z', + max: '2020-10-15T17:00:00.000Z', + }, + }, + label: 'order_date per 12 hours', + aggType: 'date_histogram', + }, + y: [ + { + accessor: 2, + format: { + id: 'number', + params: { + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }, + params: {}, + label: 'Sum of total_quantity', + aggType: 'sum', + }, + ], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + label: 'category.keyword: Descending', + aggType: 'terms', + }, + ], + }, + }, + sessionState: {}, + data: { + searchSource: { + id: 'data_source1', + requestStartHandlers: [], + inheritOptions: {}, + history: [], + fields: { + query: { + query: '', + language: 'kuery', + }, + filter: [], + index: { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + title: 'kibana_sample_data_ecommerce', + fieldFormatMap: { + taxful_total_price: { + id: 'number', + params: { + pattern: '$0,0.[00]', + }, + }, + }, + fields: [ + { + count: 0, + name: '_id', + type: 'string', + esTypes: ['_id'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: '_index', + type: 'string', + esTypes: ['_index'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: '_score', + type: 'number', + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_source', + type: '_source', + esTypes: ['_source'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_type', + type: 'string', + esTypes: ['_type'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: 'category', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'category.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'category', + }, + }, + }, + { + count: 0, + name: 'currency', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_birth_date', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_first_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'customer_first_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'customer_first_name', + }, + }, + }, + { + count: 0, + name: 'customer_full_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'customer_full_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'customer_full_name', + }, + }, + }, + { + count: 0, + name: 'customer_gender', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_id', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_last_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'customer_last_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'customer_last_name', + }, + }, + }, + { + count: 0, + name: 'customer_phone', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'day_of_week', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'day_of_week_i', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'email', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'event.dataset', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.city_name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.continent_name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.country_iso_code', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.location', + type: 'geo_point', + esTypes: ['geo_point'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.region_name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'manufacturer', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'manufacturer.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'manufacturer', + }, + }, + }, + { + count: 0, + name: 'order_date', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'order_id', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products._id', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products._id.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products._id', + }, + }, + }, + { + count: 0, + name: 'products.base_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.base_unit_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.category', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products.category.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products.category', + }, + }, + }, + { + count: 0, + name: 'products.created_on', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.discount_amount', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.discount_percentage', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.manufacturer', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products.manufacturer.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products.manufacturer', + }, + }, + }, + { + count: 0, + name: 'products.min_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.product_id', + type: 'number', + esTypes: ['long'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.product_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products.product_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products.product_name', + }, + }, + }, + { + count: 0, + name: 'products.quantity', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.sku', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.tax_amount', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.taxful_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.taxless_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.unit_discount_amount', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'sku', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'taxful_total_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'taxless_total_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'total_quantity', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'total_unique_products', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'type', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'user', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + ], + timeFieldName: 'order_date', + metaFields: ['_source', '_id', '_type', '_index', '_score'], + version: 'WzEzLDFd', + originalSavedObjectBody: { + title: 'kibana_sample_data_ecommerce', + timeFieldName: 'order_date', + fields: + '[{"count":0,"name":"_id","type":"string","esTypes":["_id"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_index","type":"string","esTypes":["_index"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_score","type":"number","scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_source","type":"_source","esTypes":["_source"],"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_type","type":"string","esTypes":["_type"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"category","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"category.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"category"}}},{"count":0,"name":"currency","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_birth_date","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_first_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_first_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_first_name"}}},{"count":0,"name":"customer_full_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_full_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_full_name"}}},{"count":0,"name":"customer_gender","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_id","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_last_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_last_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_last_name"}}},{"count":0,"name":"customer_phone","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"day_of_week","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"day_of_week_i","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"email","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"event.dataset","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.city_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.continent_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.country_iso_code","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.location","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.region_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"manufacturer","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"manufacturer.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"manufacturer"}}},{"count":0,"name":"order_date","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"order_id","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products._id","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products._id.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products._id"}}},{"count":0,"name":"products.base_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.base_unit_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.category","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.category.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.category"}}},{"count":0,"name":"products.created_on","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.discount_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.discount_percentage","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.manufacturer","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.manufacturer.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.manufacturer"}}},{"count":0,"name":"products.min_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.product_id","type":"number","esTypes":["long"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.product_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.product_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.product_name"}}},{"count":0,"name":"products.quantity","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.sku","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.tax_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.taxful_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.taxless_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.unit_discount_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"sku","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"taxful_total_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"taxless_total_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"total_quantity","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"total_unique_products","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"type","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"user","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', + fieldFormatMap: + '{"taxful_total_price":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5801","pathname":"/app/visualize","basePath":""},"pattern":"$0,0.[00]"}}}', + }, + shortDotsEnable: false, + fieldFormats: { + fieldFormats: {}, + defaultMap: { + ip: { + id: 'ip', + params: {}, + }, + date: { + id: 'date', + params: {}, + }, + date_nanos: { + id: 'date_nanos', + params: {}, + es: true, + }, + number: { + id: 'number', + params: {}, + }, + boolean: { + id: 'boolean', + params: {}, + }, + _source: { + id: '_source', + params: {}, + }, + _default_: { + id: 'string', + params: {}, + }, + }, + metaParamsOptions: {}, + }, + }, + }, + dependencies: { + legacy: { + loadingCount$: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + destination: { + closed: true, + }, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 13, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 3, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + }, + }, + aggs: { + typesRegistry: {}, + getResponseAggs: () => [ + { + id: '1', + enabled: true, + type: 'sum', + params: { + field: 'total_quantity', + }, + schema: 'metric', + toSerializedFieldFormat: () => ({ + id: 'number', + params: { + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }), + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { + from: '2020-09-30T12:41:13.795Z', + to: '2020-10-15T17:00:00.000Z', + }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + toSerializedFieldFormat: () => ({ + id: 'date', + params: { pattern: 'HH:mm:ss.SSS' }, + }), + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'category.keyword', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + toSerializedFieldFormat: () => ({ + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }), + }, + ], + }, + }, + isHierarchical: () => false, + uiState: {}, +}; diff --git a/src/plugins/vis_type_vislib/public/services.ts b/src/plugins/vis_type_vislib/public/services.ts index 7257b98f2e9f5..633fae9c7f2a6 100644 --- a/src/plugins/vis_type_vislib/public/services.ts +++ b/src/plugins/vis_type_vislib/public/services.ts @@ -19,7 +19,6 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; export const [getDataActions, setDataActions] = createGetterSetter< DataPublicPluginStart['actions'] @@ -28,7 +27,3 @@ export const [getDataActions, setDataActions] = createGetterSetter< export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('vislib data.fieldFormats'); - -export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( - 'vislib kibanalegacy' -); diff --git a/src/plugins/vis_type_vislib/public/to_ast.test.ts b/src/plugins/vis_type_vislib/public/to_ast.test.ts new file mode 100644 index 0000000000000..48d3dfe254d0b --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpression } from '../../expressions/public'; + +import { BasicVislibParams } from './types'; +import { toExpressionAst } from './to_ast'; +import { sampleAreaVis } from './sample_vis.test.mocks'; + +jest.mock('../../expressions/public', () => ({ + ...(jest.requireActual('../../expressions/public') as any), + buildExpression: jest.fn().mockImplementation(() => ({ + toAst: () => ({ + type: 'expression', + chain: [], + }), + })), +})); + +jest.mock('./to_ast_esaggs', () => ({ + getEsaggsFn: jest.fn(), +})); + +describe('vislib vis toExpressionAst function', () => { + let vis: Vis; + + const params = { + timefilter: {}, + timeRange: {}, + abortSignal: {}, + } as any; + + beforeEach(() => { + vis = sampleAreaVis as any; + }); + + it('should match basic snapshot', () => { + toExpressionAst(vis, params); + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + + expect(builtExpression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/to_ast.ts b/src/plugins/vis_type_vislib/public/to_ast.ts new file mode 100644 index 0000000000000..7cd55ccd32ebc --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +import { VisToExpressionAst, getVisSchemas } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; + +import { vislibVisName, VisTypeVislibExpressionFunctionDefinition } from './vis_type_vislib_vis_fn'; +import { BasicVislibParams } from './types'; +import { + DateHistogramParams, + Dimensions, + HistogramParams, +} from './vislib/helpers/point_series/point_series'; +import { getEsaggsFn } from './to_ast_esaggs'; + +export const toExpressionAst: VisToExpressionAst = async (vis, params) => { + const schemas = getVisSchemas(vis, params); + const dimensions: Dimensions = { + x: schemas.segment ? schemas.segment[0] : null, + y: schemas.metric, + z: schemas.radius, + width: schemas.width, + series: schemas.group, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const responseAggs = vis.data.aggs?.getResponseAggs() ?? []; + + if (dimensions.x) { + const xAgg = responseAggs[dimensions.x.accessor] as any; + if (xAgg.type.name === 'date_histogram') { + (dimensions.x.params as DateHistogramParams).date = true; + const { esUnit, esValue } = xAgg.buckets.getInterval(); + (dimensions.x.params as DateHistogramParams).intervalESUnit = esUnit; + (dimensions.x.params as DateHistogramParams).intervalESValue = esValue; + (dimensions.x.params as DateHistogramParams).interval = moment + .duration(esValue, esUnit) + .asMilliseconds(); + (dimensions.x.params as DateHistogramParams).format = xAgg.buckets.getScaledDateFormat(); + (dimensions.x.params as DateHistogramParams).bounds = xAgg.buckets.getBounds(); + } else if (xAgg.type.name === 'histogram') { + const intervalParam = xAgg.type.paramByName('interval'); + const output = { params: {} as any }; + await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { + abortSignal: params.abortSignal, + }); + intervalParam.write(xAgg, output); + (dimensions.x.params as HistogramParams).interval = output.params.interval; + } + } + + const visConfig = { ...vis.params }; + + (dimensions.y || []).forEach((yDimension) => { + const yAgg = responseAggs.filter(({ enabled }) => enabled)[yDimension.accessor]; + const seriesParam = (visConfig.seriesParams || []).find((param) => param.data.id === yAgg.id); + if (seriesParam) { + const usedValueAxis = (visConfig.valueAxes || []).find( + (valueAxis) => valueAxis.id === seriesParam.valueAxis + ); + if (usedValueAxis?.scale.mode === 'percentage') { + yDimension.format = { id: 'percent' }; + } + } + if (visConfig?.gauge?.percentageMode === true) { + yDimension.format = { id: 'percent' }; + } + }); + + visConfig.dimensions = dimensions; + + const configStr = JSON.stringify(visConfig).replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); + const visTypeXy = buildExpressionFunction( + vislibVisName, + { + type: vis.type.name, + visConfig: configStr, + } + ); + + const ast = buildExpression([getEsaggsFn(vis), visTypeXy]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_vislib/public/components/options/index.ts b/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts similarity index 51% rename from src/plugins/vis_type_vislib/public/components/options/index.ts rename to src/plugins/vis_type_vislib/public/to_ast_esaggs.ts index 57afbd4818ae4..a7312c9d36cbb 100644 --- a/src/plugins/vis_type_vislib/public/components/options/index.ts +++ b/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts @@ -17,8 +17,24 @@ * under the License. */ -export { GaugeOptions } from './gauge'; -export { PieOptions } from './pie'; -export { PointSeriesOptions } from './point_series'; -export { HeatmapOptions } from './heatmap'; -export { MetricsAxisOptions } from './metrics_axes'; +import { Vis } from '../../visualizations/public'; +import { buildExpressionFunction } from '../../expressions/public'; +import { EsaggsExpressionFunctionDefinition } from '../../data/public'; + +import { PieVisParams } from './pie'; +import { BasicVislibParams } from './types'; + +/** + * Get esaggs expressions function + * TODO: replace this with vis.data.aggs!.toExpressionAst(); + * @param vis + */ +export function getEsaggsFn(vis: Vis | Vis) { + return buildExpressionFunction('esaggs', { + index: vis.data.indexPattern!.id!, + metricsAtAllLevels: vis.isHierarchical(), + partialRows: false, + aggConfigs: JSON.stringify(vis.data.aggs!.aggs), + includeFormatHints: false, + }); +} diff --git a/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts b/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts new file mode 100644 index 0000000000000..36a9a17341bc5 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpression } from '../../expressions/public'; + +import { PieVisParams } from './pie'; +import { samplePieVis } from './sample_vis.test.mocks'; +import { toExpressionAst } from './to_ast_pie'; + +jest.mock('../../expressions/public', () => ({ + ...(jest.requireActual('../../expressions/public') as any), + buildExpression: jest.fn().mockImplementation(() => ({ + toAst: () => ({ + type: 'expression', + chain: [], + }), + })), +})); + +jest.mock('./to_ast_esaggs', () => ({ + getEsaggsFn: jest.fn(), +})); + +describe('vislib pie vis toExpressionAst function', () => { + let vis: Vis; + + const params = { + timefilter: {}, + timeRange: {}, + abortSignal: {}, + } as any; + + beforeEach(() => { + vis = samplePieVis as any; + }); + + it('should match basic snapshot', () => { + toExpressionAst(vis, params); + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + + expect(builtExpression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/to_ast_pie.ts b/src/plugins/vis_type_vislib/public/to_ast_pie.ts new file mode 100644 index 0000000000000..95a5f89208ef9 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast_pie.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getVisSchemas, VisToExpressionAst } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; + +import { PieVisParams } from './pie'; +import { vislibPieName, VisTypeVislibPieExpressionFunctionDefinition } from './pie_fn'; +import { getEsaggsFn } from './to_ast_esaggs'; + +export const toExpressionAst: VisToExpressionAst = async (vis, params) => { + const schemas = getVisSchemas(vis, params); + const visConfig = { + ...vis.params, + dimensions: { + metric: schemas.metric[0], + buckets: schemas.segment, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }, + }; + + const configStr = JSON.stringify(visConfig).replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); + const visTypePie = buildExpressionFunction( + vislibPieName, + { + visConfig: configStr, + } + ); + + const ast = buildExpression([getEsaggsFn(vis), visTypePie]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts index a29f0bcb5c52a..c0311edf76154 100644 --- a/src/plugins/vis_type_vislib/public/types.ts +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -29,10 +29,13 @@ import { ThresholdLineStyles, } from './utils/collections'; import { Labels, Style } from '../../charts/public'; +import { Dimensions } from './vislib/helpers/point_series/point_series'; export interface CommonVislibParams { addTooltip: boolean; + addLegend: boolean; legendPosition: Positions; + dimensions: Dimensions; } export interface Scale { @@ -87,6 +90,9 @@ export interface BasicVislibParams extends CommonVislibParams { labels: Labels; thresholdLine: ThresholdLine; valueAxes: ValueAxis[]; + gauge?: { + percentageMode: boolean; + }; grid: { categoryLines: boolean; valueAxis?: string; diff --git a/src/plugins/vis_type_vislib/public/vis_controller.tsx b/src/plugins/vis_type_vislib/public/vis_controller.tsx index 3a05030f804ca..1804d0d52ae7a 100644 --- a/src/plugins/vis_type_vislib/public/vis_controller.tsx +++ b/src/plugins/vis_type_vislib/public/vis_controller.tsx @@ -20,127 +20,147 @@ import $ from 'jquery'; import React, { RefObject } from 'react'; -import { Positions } from './utils/collections'; -import { VisTypeVislibDependencies } from './plugin'; import { mountReactNode } from '../../../core/public/utils'; +import { ChartsPluginSetup } from '../../charts/public'; +import { PersistedState } from '../../visualizations/public'; +import { IInterpreterRenderHandlers } from '../../expressions/public'; + +import { VisTypeVislibCoreSetup } from './plugin'; import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; -import { VisParams, ExprVis } from '../../visualizations/public'; -import { getKibanaLegacy } from './services'; +import { BasicVislibParams } from './types'; +import { PieVisParams } from './pie'; const legendClassName = { - top: 'visLib--legend-top', - bottom: 'visLib--legend-bottom', - left: 'visLib--legend-left', - right: 'visLib--legend-right', + top: 'vislib--legend-top', + bottom: 'vislib--legend-bottom', + left: 'vislib--legend-left', + right: 'vislib--legend-right', }; -export const createVislibVisController = (deps: VisTypeVislibDependencies) => { +export type VislibVisController = InstanceType>; + +export const createVislibVisController = ( + core: VisTypeVislibCoreSetup, + charts: ChartsPluginSetup +) => { return class VislibVisController { - unmount: (() => void) | null = null; - visParams?: VisParams; + private removeListeners?: () => void; + private unmountLegend?: () => void; + legendRef: RefObject; container: HTMLDivElement; chartEl: HTMLDivElement; legendEl: HTMLDivElement; - vislibVis: any; + vislibVis?: any; - constructor(public el: Element, public vis: ExprVis) { + constructor(public el: HTMLDivElement) { this.el = el; - this.vis = vis; - this.unmount = null; this.legendRef = React.createRef(); // vis mount point this.container = document.createElement('div'); - this.container.className = 'visLib'; + this.container.className = 'vislib'; this.el.appendChild(this.container); // chart mount point this.chartEl = document.createElement('div'); - this.chartEl.className = 'visLib__chart'; + this.chartEl.className = 'vislib__chart'; this.container.appendChild(this.chartEl); // legend mount point this.legendEl = document.createElement('div'); - this.legendEl.className = 'visLib__legend'; + this.legendEl.className = 'vislib__legend'; this.container.appendChild(this.legendEl); } - render(esResponse: any, visParams: VisParams): Promise { + async render( + esResponse: any, + visParams: BasicVislibParams | PieVisParams, + handlers: IInterpreterRenderHandlers + ): Promise { if (this.vislibVis) { - this.destroy(); + this.destroy(false); + } + + // Used in functional tests to know when chart is loaded by type + this.chartEl.dataset.vislibChartType = visParams.type; + + if (this.el.clientWidth === 0 || this.el.clientHeight === 0) { + handlers.done(); + return; + } + + const [, { kibanaLegacy }] = await core.getStartServices(); + kibanaLegacy.loadFontAwesome(); + + // @ts-expect-error + const { Vis: Vislib } = await import('./vislib/vis'); + const { uiState, event: fireEvent } = handlers; + + this.vislibVis = new Vislib(this.chartEl, visParams, core, charts); + this.vislibVis.on('brush', fireEvent); + this.vislibVis.on('click', fireEvent); + this.vislibVis.on('renderComplete', handlers.done); + this.removeListeners = () => { + this.vislibVis.off('brush', fireEvent); + this.vislibVis.off('click', fireEvent); + }; + + this.vislibVis.initVisConfig(esResponse, uiState); + + if (visParams.addLegend) { + $(this.container) + .attr('class', (i, cls) => { + return cls.replace(/vislib--legend-\S+/g, ''); + }) + .addClass((legendClassName as any)[visParams.legendPosition]); + + this.mountLegend(esResponse, visParams, fireEvent, uiState); } - getKibanaLegacy().loadFontAwesome(); - - return new Promise(async (resolve) => { - if (this.el.clientWidth === 0 || this.el.clientHeight === 0) { - return resolve(); - } - - // @ts-expect-error - const { Vis: Vislib } = await import('./vislib/vis'); - - this.vislibVis = new Vislib(this.chartEl, visParams, deps); - this.vislibVis.on('brush', this.vis.API.events.brush); - this.vislibVis.on('click', this.vis.API.events.filter); - this.vislibVis.on('renderComplete', resolve); - - this.vislibVis.initVisConfig(esResponse, this.vis.getUiState()); - - if (visParams.addLegend) { - $(this.container) - .attr('class', (i, cls) => { - return cls.replace(/visLib--legend-\S+/g, ''); - }) - .addClass((legendClassName as any)[visParams.legendPosition]); - - this.mountLegend(esResponse, visParams.legendPosition); - } - - this.vislibVis.render(esResponse, this.vis.getUiState()); - - // refreshing the legend after the chart is rendered. - // this is necessary because some visualizations - // provide data necessary for the legend only after a render cycle. - if ( - visParams.addLegend && - CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type) - ) { - this.unmountLegend(); - this.mountLegend(esResponse, visParams.legendPosition); - this.vislibVis.render(esResponse, this.vis.getUiState()); - } - }); + this.vislibVis.render(esResponse, uiState); + + // refreshing the legend after the chart is rendered. + // this is necessary because some visualizations + // provide data necessary for the legend only after a render cycle. + if ( + visParams.addLegend && + CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type) + ) { + this.unmountLegend?.(); + this.mountLegend(esResponse, visParams, fireEvent, uiState); + this.vislibVis.render(esResponse, uiState); + } } - mountLegend(visData: any, position: Positions) { - this.unmount = mountReactNode( + mountLegend( + visData: unknown, + { legendPosition, addLegend }: BasicVislibParams | PieVisParams, + fireEvent: IInterpreterRenderHandlers['event'], + uiState?: PersistedState + ) { + this.unmountLegend = mountReactNode( )(this.legendEl); } - unmountLegend() { - if (this.unmount) { - this.unmount(); - } - } + destroy(clearElement = true) { + this.unmountLegend?.(); - destroy() { - if (this.unmount) { - this.unmount(); + if (clearElement) { + this.el.innerHTML = ''; } if (this.vislibVis) { - this.vislibVis.off('brush', this.vis.API.events.brush); - this.vislibVis.off('click', this.vis.API.events.filter); + this.removeListeners?.(); this.vislibVis.destroy(); delete this.vislibVis; } diff --git a/src/plugins/vis_type_vislib/public/vis_renderer.tsx b/src/plugins/vis_type_vislib/public/vis_renderer.tsx new file mode 100644 index 0000000000000..9c697f481e63e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vis_renderer.tsx @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { ExpressionRenderDefinition } from '../../expressions/public'; +import { VisualizationContainer } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +import { VisTypeVislibCoreSetup } from './plugin'; +import { VislibRenderValue, vislibVisName } from './vis_type_vislib_vis_fn'; + +function shouldShowNoResultsMessage(visData: any, visType: string): boolean { + if (['goal', 'gauge'].includes(visType)) { + return false; + } + + const rows: object[] | undefined = visData?.rows; + const isZeroHits = visData?.hits === 0 || (rows && !rows.length); + + return Boolean(isZeroHits); +} + +const VislibWrapper = lazy(() => import('./vis_wrapper')); + +export const getVislibVisRenderer: ( + core: VisTypeVislibCoreSetup, + charts: ChartsPluginSetup +) => ExpressionRenderDefinition = (core, charts) => ({ + name: vislibVisName, + reuseDomNode: true, + render: async (domNode, config, handlers) => { + const showNoResult = shouldShowNoResultsMessage(config.visData, config.visType); + + handlers.onDestroy(() => unmountComponentAtNode(domNode)); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index 557f9930f55b1..c5fa8f36f43e3 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -18,29 +18,35 @@ */ import { i18n } from '@kbn/i18n'; + import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; + // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; +import { BasicVislibParams } from './types'; + +export const vislibVisName = 'vislib_vis'; interface Arguments { type: string; visConfig: string; } -type VisParams = Required; - -interface RenderValue { +export interface VislibRenderValue { + visData: any; visType: string; - visConfig: VisParams; + visConfig: BasicVislibParams; } -export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition< - 'vislib', +export type VisTypeVislibExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof vislibVisName, Datatable, Arguments, - Render -> => ({ - name: 'vislib', + Render +>; + +export const createVisTypeVislibVisFn = (): VisTypeVislibExpressionFunctionDefinition => ({ + name: vislibVisName, type: 'render', inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.vislib.help', { @@ -55,23 +61,21 @@ export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition< visConfig: { types: ['string'], default: '"{}"', - help: '', + help: 'vislib vis config', }, }, fn(context, args) { - const visConfigParams = JSON.parse(args.visConfig); - const convertedData = vislibSeriesResponseHandler(context, visConfigParams.dimensions); + const visType = args.type; + const visConfig = JSON.parse(args.visConfig) as BasicVislibParams; + const visData = vislibSeriesResponseHandler(context, visConfig.dimensions); return { type: 'render', - as: 'visualization', + as: vislibVisName, value: { - visData: convertedData, - visType: args.type, - visConfig: visConfigParams, - params: { - listenOnChange: true, - }, + visData, + visConfig, + visType, }, }; }, diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts index f44d503895483..1b43a213c618d 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts @@ -17,11 +17,22 @@ * under the License. */ -export { createHistogramVisTypeDefinition } from './histogram'; -export { createLineVisTypeDefinition } from './line'; -export { createPieVisTypeDefinition } from './pie'; -export { createAreaVisTypeDefinition } from './area'; -export { createHeatmapVisTypeDefinition } from './heatmap'; -export { createHorizontalBarVisTypeDefinition } from './horizontal_bar'; -export { createGaugeVisTypeDefinition } from './gauge'; -export { createGoalVisTypeDefinition } from './goal'; +import { histogramVisTypeDefinition } from './histogram'; +import { lineVisTypeDefinition } from './line'; +import { areaVisTypeDefinition } from './area'; +import { heatmapVisTypeDefinition } from './heatmap'; +import { horizontalBarVisTypeDefinition } from './horizontal_bar'; +import { gaugeVisTypeDefinition } from './gauge'; +import { goalVisTypeDefinition } from './goal'; + +export { pieVisTypeDefinition } from './pie'; + +export const visLibVisTypeDefinitions = [ + histogramVisTypeDefinition, + lineVisTypeDefinition, + areaVisTypeDefinition, + heatmapVisTypeDefinition, + horizontalBarVisTypeDefinition, + gaugeVisTypeDefinition, + goalVisTypeDefinition, +]; diff --git a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx new file mode 100644 index 0000000000000..980ba1c175885 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useMemo, useRef } from 'react'; +import { EuiResizeObserver } from '@elastic/eui'; +import { debounce } from 'lodash'; + +import { IInterpreterRenderHandlers } from '../../expressions/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +import { VislibRenderValue } from './vis_type_vislib_vis_fn'; +import { createVislibVisController, VislibVisController } from './vis_controller'; +import { VisTypeVislibCoreSetup } from './plugin'; + +import './index.scss'; + +type VislibWrapperProps = VislibRenderValue & { + core: VisTypeVislibCoreSetup; + charts: ChartsPluginSetup; + handlers: IInterpreterRenderHandlers; +}; + +const VislibWrapper = ({ core, charts, visData, visConfig, handlers }: VislibWrapperProps) => { + const chartDiv = useRef(null); + const visController = useRef(null); + + const updateChart = useMemo( + () => + debounce(() => { + if (visController.current) { + visController.current.render(visData, visConfig, handlers); + } + }, 100), + [visConfig, visData, handlers] + ); + + useEffect(() => { + if (chartDiv.current) { + const Controller = createVislibVisController(core, charts); + visController.current = new Controller(chartDiv.current); + } + return () => { + visController.current?.destroy(); + visController.current = null; + }; + }, [core, charts, handlers]); + + useEffect(updateChart, [updateChart]); + + useEffect(() => { + if (handlers.uiState) { + handlers.uiState.on('change', updateChart); + + return () => { + handlers.uiState?.off('change', updateChart); + }; + } + }, [handlers.uiState, updateChart]); + + return ( + + {(resizeRef) => ( +
+
+
+ )} + + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VislibWrapper as default }; diff --git a/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss index c03aa19140de0..843bb9d3f03eb 100644 --- a/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss +++ b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss @@ -1,27 +1,29 @@ -.visLib { +.vislib { flex: 1 1 0; display: flex; flex-direction: row; overflow: auto; - &.visLib--legend-left { + &.vislib--legend-left { flex-direction: row-reverse; } - &.visLib--legend-right { + &.vislib--legend-right { flex-direction: row; } - &.visLib--legend-top { + &.vislib--legend-top { flex-direction: column-reverse; } - &.visLib--legend-bottom { + &.vislib--legend-bottom { flex-direction: column; } } -.visLib__chart { +.vislib__chart, +.vislib__wrapper, +.vislib__container { display: flex; flex: 1 1 auto; min-height: 0; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss index b1a59f88a348a..a06f0cb00787b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss @@ -13,6 +13,7 @@ $visLegendLineHeight: $euiSize; left: 0; display: flex; padding: $euiSizeXS; + margin: $euiSizeS; background-color: $euiColorEmptyShade; transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance, background-color $euiAnimSpeedFast $euiAnimSlightResistance $euiAnimSpeedExtraSlow; @@ -33,13 +34,13 @@ $visLegendLineHeight: $euiSize; height: 100%; } -.visLib--legend-left { +.vislib--legend-left { .visLegend__list { margin-bottom: $euiSizeL; } } -.visLib--legend-bottom { +.vislib--legend-bottom { .visLegend__list { margin-left: $euiSizeL; } @@ -64,8 +65,8 @@ $visLegendLineHeight: $euiSize; } } - .visLib--legend-top &, - .visLib--legend-bottom & { + .vislib--legend-top &, + .vislib--legend-bottom & { width: auto; flex-direction: row; flex-wrap: wrap; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 65d148cfc5ef4..a3fb536d0aec5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -41,16 +41,8 @@ jest.mock('../../../services', () => ({ }), })); -const vis = { - params: { - addLegend: true, - }, - API: { - events: { - filter: jest.fn(), - }, - }, -}; +const fireEvent = jest.fn(); + const vislibVis = { handler: { highlight: jest.fn(), @@ -96,14 +88,15 @@ const uiState = { set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)), emit: jest.fn(), setSilent: jest.fn(), -}; +} as any; const getWrapper = async (props?: Partial) => { const wrapper = mount( { }); it('should work with no handlers set', () => { - const newVis = { - ...vis, + const newProps = { vislibVis: { ...vislibVis, handler: null, @@ -197,7 +189,7 @@ describe('VisLegend Component', () => { }; expect(async () => { - wrapper = await getWrapper({ vis: newVis }); + wrapper = await getWrapper(newProps); const first = getLegendItems(wrapper).first(); first.simulate('focus'); first.simulate('blur'); @@ -216,8 +208,11 @@ describe('VisLegend Component', () => { const filterGroup = wrapper.find(EuiButtonGroup).first(); filterGroup.getElement().props.onChange('filterIn'); - expect(vis.API.events.filter).toHaveBeenCalledWith({ data: ['valuesA'], negate: false }); - expect(vis.API.events.filter).toHaveBeenCalledTimes(1); + expect(fireEvent).toHaveBeenCalledWith({ + name: 'filterBucket', + data: { data: ['valuesA'], negate: false }, + }); + expect(fireEvent).toHaveBeenCalledTimes(1); }); it('should filter in when clicked', () => { @@ -226,8 +221,11 @@ describe('VisLegend Component', () => { const filterGroup = wrapper.find(EuiButtonGroup).first(); filterGroup.getElement().props.onChange('filterOut'); - expect(vis.API.events.filter).toHaveBeenCalledWith({ data: ['valuesA'], negate: true }); - expect(vis.API.events.filter).toHaveBeenCalledTimes(1); + expect(fireEvent).toHaveBeenCalledWith({ + name: 'filterBucket', + data: { data: ['valuesA'], negate: true }, + }); + expect(fireEvent).toHaveBeenCalledTimes(1); }); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 5a2db2d21c6fe..cec97f0cadf11 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -23,16 +23,21 @@ import { compact, uniqBy, map, every, isUndefined } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keys, htmlIdGenerator } from '@elastic/eui'; +import { PersistedState } from '../../../../../visualizations/public'; +import { IInterpreterRenderHandlers } from '../../../../../expressions/public'; + import { getDataActions } from '../../../services'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; +import { BasicVislibParams } from '../../../types'; export interface VisLegendProps { - vis: any; vislibVis: any; - visData: any; - uiState: any; + visData: unknown; + uiState?: PersistedState; + fireEvent: IInterpreterRenderHandlers['event']; + addLegend: BasicVislibParams['addLegend']; position: 'top' | 'bottom' | 'left' | 'right'; } @@ -49,7 +54,10 @@ export class VisLegend extends PureComponent { constructor(props: VisLegendProps) { super(props); - const open = props.uiState.get('vis.legendOpen', true); + + // TODO: Check when this bwc can safely be removed + const bwcLegendStateDefault = props.addLegend ?? true; + const open = props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean; this.state = { open, @@ -64,13 +72,9 @@ export class VisLegend extends PureComponent { } toggleLegend = () => { - const bwcAddLegend = this.props.vis.params.addLegend; - const bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend; - const newOpen = !this.props.uiState.get('vis.legendOpen', bwcLegendStateDefault); - this.setState({ open: newOpen }); - // open should be applied on template before we update uiState - setTimeout(() => { - this.props.uiState.set('vis.legendOpen', newOpen); + const newOpen = !this.state.open; + this.setState({ open: newOpen }, () => { + this.props.uiState?.set('vis.legendOpen', newOpen); }); }; @@ -79,17 +83,23 @@ export class VisLegend extends PureComponent { return; } - const colors = this.props.uiState.get('vis.colors') || {}; + const colors = this.props.uiState?.get('vis.colors') || {}; if (colors[label] === color) delete colors[label]; else colors[label] = color; - this.props.uiState.setSilent('vis.colors', null); - this.props.uiState.set('vis.colors', colors); - this.props.uiState.emit('colorChanged'); + this.props.uiState?.setSilent('vis.colors', null); + this.props.uiState?.set('vis.colors', colors); + this.props.uiState?.emit('colorChanged'); this.refresh(); }; filter = ({ values: data }: LegendItem, negate: boolean) => { - this.props.vis.API.events.filter({ data, negate }); + this.props.fireEvent({ + name: 'filterBucket', + data: { + data, + negate, + }, + }); }; canFilter = async (item: LegendItem): Promise => { @@ -172,11 +182,8 @@ export class VisLegend extends PureComponent { return; } // make sure vislib is defined at this point - if ( - this.props.uiState.get('vis.legendOpen') == null && - this.props.vis.params.addLegend != null - ) { - this.setState({ open: this.props.vis.params.addLegend }); + if (this.props.uiState?.get('vis.legendOpen') == null && this.props.addLegend != null) { + this.setState({ open: this.props.addLegend }); } if (vislibVis.visConfig) { diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts index 32536960c59cd..147bae7e3b985 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts @@ -33,11 +33,11 @@ export function initXAxis(chart: Chart, table: Table) { chart.xAxisLabel = title; if ('interval' in params) { - const { interval } = params; if ('date' in params) { const { intervalESUnit, intervalESValue } = params; + chart.ordered = { - interval: moment.duration(interval), + interval: moment.duration(intervalESValue, intervalESUnit as any), intervalESUnit, intervalESValue, }; diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts index 4ba3101d34898..f40d01e6a8123 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts @@ -28,7 +28,7 @@ import { Column, Table } from '../../types'; export interface DateHistogramParams { date: boolean; - interval: string; + interval: number | string; intervalESValue: number; intervalESUnit: string; format: string; @@ -57,6 +57,9 @@ export interface Dimensions { y: Dimension[]; z?: Dimension[]; series?: Dimension | Dimension[]; + width?: Dimension[]; + splitRow?: Dimension[]; + splitColumn?: Dimension[]; } export interface Aspect { accessor: Column['id']; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss deleted file mode 100644 index d1c7fc7c6278c..0000000000000 --- a/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss +++ /dev/null @@ -1,17 +0,0 @@ -.visError { - flex: 1 1 0; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - - // From ML - .top { align-self: flex-start; } - .bottom { align-self: flex-end; } -} - -// Prevent large request errors from overflowing the container -.visError--request { - max-width: 100%; - max-height: 100%; -} diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss index b19c2dfb153b9..6751e9f28a8ee 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss +++ b/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss @@ -1,4 +1,3 @@ @import './alerts'; -@import './handler'; @import './layout/index'; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js index 4c50472b9d11a..16283c6bddf26 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js @@ -182,7 +182,6 @@ export class Dispatch { const data = d.input || d; return { - e: d3.event, data: isSlices ? this._pieClickResponse(data) : this._seriesClickResponse(data), }; } @@ -423,7 +422,6 @@ export class Dispatch { */ createBrush(xScale, svg) { const self = this; - const visConfig = self.handler.visConfig; const { width, height } = svg.node().getBBox(); const isHorizontal = self.handler.categoryAxes[0].axisConfig.isHorizontal(); @@ -449,8 +447,6 @@ export class Dispatch { return self.emit('brush', { range, - config: visConfig, - e: d3.event, data, }); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js index 3c1aeaa0d1d0d..938ea3adcb9b5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -46,10 +46,10 @@ const markdownIt = new MarkdownIt({ * create the visualization */ export class Handler { - constructor(vis, visConfig, deps) { + constructor(vis, visConfig, uiSettings) { this.el = visConfig.get('el'); this.ChartClass = chartTypes[visConfig.get('type')]; - this.deps = deps; + this.uiSettings = uiSettings; this.charts = []; this.vis = vis; @@ -91,12 +91,18 @@ export class Handler { const xRaw = _.get(eventPayload.data, 'series[0].values[0].xRaw'); if (!xRaw) return; // not sure if this is possible? return self.vis.emit(eventType, { - table: xRaw.table, - range: eventPayload.range, - column: xRaw.column, + name: 'brush', + data: { + table: xRaw.table, + range: eventPayload.range, + column: xRaw.column, + }, }); case 'click': - return self.vis.emit(eventType, eventPayload); + return self.vis.emit(eventType, { + name: 'filterBucket', + data: eventPayload, + }); } }; }); @@ -164,7 +170,7 @@ export class Handler { let loadedCount = 0; const chartSelection = selection.selectAll('.chart'); chartSelection.each(function (chartData) { - const chart = new self.ChartClass(self, this, chartData, self.deps); + const chart = new self.ChartClass(self, this, chartData, self.uiSettings); self.vis.eventNames().forEach(function (event) { self.enable(event, chart); @@ -222,7 +228,7 @@ export class Handler { // class name needs `chart` in it for the polling checkSize function // to continuously call render on resize .attr('class', 'visError chart error') - .attr('data-test-subj', 'visLibVisualizeError'); + .attr('data-test-subj', 'vislibVisualizeError'); div.append('h4').text(markdownIt.renderInline(message)); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js index d50c70de1bb48..119a24d2f25d1 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js @@ -157,7 +157,7 @@ dateHistogramArray.forEach(function (data, i) { const args = Array.from(arguments); expect(args.length).toBe(2); expect(args[0]).toBe('click'); - expect(args[1]).toBe(event); + expect(args[1].data).toBe(event); done(); }; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js index dda9d85ec43c5..2086f744be584 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js @@ -50,7 +50,6 @@ export class VisConfig { return _.get(this._values, property, defaults); } else { throw new Error(`Accessing invalid config property: ${property}`); - return defaults; } } diff --git a/src/plugins/vis_type_vislib/public/vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.js index f258cb55ba281..628b876fc50c5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/vis.js +++ b/src/plugins/vis_type_vislib/public/vislib/vis.js @@ -35,13 +35,14 @@ import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../../comm * @param config {Object} Parameters that define the chart type and chart options */ export class Vis extends EventEmitter { - constructor(element, visConfigArgs, deps) { + constructor(element, visConfigArgs, core, charts) { super(); this.element = element.get ? element.get(0) : element; this.visConfigArgs = _.cloneDeep(visConfigArgs); - this.visConfigArgs.dimmingOpacity = deps.uiSettings.get(DIMMING_OPACITY_SETTING); - this.visConfigArgs.heatmapMaxBuckets = deps.uiSettings.get(HEATMAP_MAX_BUCKETS_SETTING); - this.deps = deps; + this.visConfigArgs.dimmingOpacity = core.uiSettings.get(DIMMING_OPACITY_SETTING); + this.visConfigArgs.heatmapMaxBuckets = core.uiSettings.get(HEATMAP_MAX_BUCKETS_SETTING); + this.charts = charts; + this.uiSettings = core.uiSettings; } hasLegend() { @@ -56,7 +57,7 @@ export class Vis extends EventEmitter { this.data, this.uiState, this.element, - this.deps.charts.colors.createColorLookupFunction.bind(this.deps.charts.colors) + this.charts.colors.createColorLookupFunction.bind(this.charts.colors) ); } @@ -78,7 +79,7 @@ export class Vis extends EventEmitter { this.initVisConfig(data, uiState); - this.handler = new Handler(this, this.visConfig, this.deps); + this.handler = new Handler(this, this.visConfig, this.uiSettings); this._runOnHandler('render'); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js index 5ed6d3eb79f4b..e5cb0235b6510 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js @@ -39,13 +39,13 @@ import { * @param chartData {Object} Elasticsearch query results for this specific chart */ export class Chart { - constructor(handler, element, chartData, deps) { + constructor(handler, element, chartData, uiSettings) { this.handler = handler; this.chartEl = element; this.chartData = chartData; this.tooltips = []; - const events = (this.events = new Dispatch(handler, deps.uiSettings)); + const events = (this.events = new Dispatch(handler, uiSettings)); const fieldFormatter = getFormatService().deserialize( this.handler.data.get('tooltipFormatter') diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js index 0ffa53fc7ca9c..11b5964eebdf3 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js @@ -53,20 +53,10 @@ afterEach(function () { count = 0; }); -const getDeps = () => { - const mockUiSettings = coreMock.createSetup().uiSettings; - const charts = chartPluginMock.createStartContract(); - - return { - uiSettings: mockUiSettings, - charts: charts, - }; -}; - -export function getVis(visLibParams, element) { +export function getVis(vislibParams, element) { return new Vis( element || $visCanvas.new(), - _.defaults({}, visLibParams || {}, { + _.defaults({}, vislibParams || {}, { addTooltip: true, addLegend: true, defaultYExtents: false, @@ -74,6 +64,7 @@ export function getVis(visLibParams, element) { yAxis: {}, type: 'histogram', }), - getDeps() + coreMock.createSetup(), + chartPluginMock.createStartContract() ); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js index 6fdc2a134b820..913b237a10e58 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js @@ -28,7 +28,7 @@ import { getVis } from './_vis_fixture'; describe('Vislib Gauge Chart Test Suite', function () { let vis; let chartEl; - const visLibParams = { + const vislibParams = { type: 'gauge', addTooltip: true, addLegend: false, @@ -71,7 +71,7 @@ describe('Vislib Gauge Chart Test Suite', function () { }; function generateVis(opts = {}) { - const config = _.defaultsDeep({}, opts, visLibParams); + const config = _.defaultsDeep({}, opts, vislibParams); if (vis) { vis.destroy(); $('.visChart').remove(); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js index 938d3d0ec6d74..b1acea34aabf6 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js @@ -42,8 +42,8 @@ const defaults = { * @param chartData {Object} Elasticsearch query results for this specific chart */ export class PieChart extends Chart { - constructor(handler, chartEl, chartData, deps) { - super(handler, chartEl, chartData, deps); + constructor(handler, chartEl, chartData, uiSettings) { + super(handler, chartEl, chartData, uiSettings); const charts = this.handler.data.getVisData(); this._validatePieData(charts); this._attr = _.defaults(handler.visConfig.get('chart', {}), defaults); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js index e2da33d0808ba..1207db2e54bb8 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js @@ -40,7 +40,7 @@ let mockedSVGElementGetBBox; let mockedSVGElementGetComputedTextLength; describe('No global chart settings', function () { - const visLibParams1 = { + const vislibParams1 = { el: '
', type: 'pie', addLegend: true, @@ -58,7 +58,7 @@ describe('No global chart settings', function () { }); beforeEach(() => { - chart1 = getVis(visLibParams1); + chart1 = getVis(vislibParams1); mockUiState = getMockUiState(); }); @@ -153,7 +153,7 @@ describe('Vislib PieChart Class Test Suite', function () { describe('Vislib PieChart Class Test Suite for ' + names[i] + ' data', function () { const mockPieData = pieChartMockData[aggItem]; - const visLibParams = { + const vislibParams = { type: 'pie', addLegend: true, addTooltip: true, @@ -161,7 +161,7 @@ describe('Vislib PieChart Class Test Suite', function () { let vis; beforeEach(async () => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); const mockUiState = getMockUiState(); vis.render(mockPieData, mockUiState); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js index 9a25d041f6567..a40d46737f05e 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js @@ -40,10 +40,10 @@ const touchdownTmpl = _.template(touchdownTmplHtml); * @param chartData {Object} Elasticsearch query results for this specific chart */ export class PointSeries extends Chart { - constructor(handler, chartEl, chartData, deps) { - super(handler, chartEl, chartData, deps); + constructor(handler, chartEl, chartData, uiSettings) { + super(handler, chartEl, chartData, uiSettings); - this.deps = deps; + this.uiSettings = uiSettings; this.handler = handler; this.chartData = chartData; this.chartEl = chartEl; @@ -246,7 +246,13 @@ export class PointSeries extends Chart { if (!seriArgs.show) return; const SeriClass = seriTypes[seriArgs.type || self.handler.visConfig.get('chart.type')] || seriTypes.line; - const series = new SeriClass(self.handler, svg, data.series[i], seriArgs, self.deps); + const series = new SeriClass( + self.handler, + svg, + data.series[i], + seriArgs, + self.uiSettings + ); series.events = self.events; svg.call(series.draw()); self.series.push(series); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js index e3e2d31ecd4f4..b65c5be330fef 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js @@ -43,8 +43,8 @@ const defaults = { * chart */ export class AreaChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); + constructor(handler, chartEl, chartData, seriesConfigArgs, core) { + super(handler, chartEl, chartData, seriesConfigArgs, core); this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); this.isOverlapping = this.seriesConfig.mode !== 'stacked'; diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js index 3cd58060978ee..ae15d95d560ca 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js @@ -38,7 +38,7 @@ const dataTypesArray = { stackedSeries: import('../../../fixtures/mock_data/date_histogram/_stacked_series'), }; -const visLibParams = { +const vislibParams = { type: 'area', addLegend: true, addTooltip: true, @@ -61,7 +61,7 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) { }); beforeEach(async () => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(await dataType, mockUiState); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js index 1369bf1dff68a..07bebf4eb2f83 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js @@ -57,8 +57,8 @@ function datumWidth(defaultWidth, datum, nextDatum, scale, gutterWidth, groupCou * @param chartData {Object} Elasticsearch query results for this specific chart */ export class ColumnChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); + constructor(handler, chartEl, chartData, seriesConfigArgs, core) { + super(handler, chartEl, chartData, seriesConfigArgs, core); this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); this.labelOptions = _.defaults(handler.visConfig.get('labels', {}), defaults.showLabel); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js index f3d8d66df2d85..d7fc177a30009 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js @@ -62,7 +62,7 @@ dataTypesArray.forEach(function (dataType) { describe('Vislib Column Chart Test Suite for ' + name + ' Data', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -81,7 +81,7 @@ dataTypesArray.forEach(function (dataType) { }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(data, mockUiState); @@ -261,7 +261,7 @@ dataTypesArray.forEach(function (dataType) { describe('stackData method - data set with zeros in percentage mode', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -276,7 +276,7 @@ describe('stackData method - data set with zeros in percentage mode', function ( }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); }); @@ -320,7 +320,7 @@ describe('stackData method - data set with zeros in percentage mode', function ( describe('datumWidth - split chart data set with holes', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -335,7 +335,7 @@ describe('datumWidth - split chart data set with holes', function () { }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(rowsSeriesWithHoles, mockUiState); @@ -366,7 +366,7 @@ describe('datumWidth - split chart data set with holes', function () { describe('datumWidth - monthly interval', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -384,7 +384,7 @@ describe('datumWidth - monthly interval', function () { }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(seriesMonthlyInterval, mockUiState); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js index 8c727d225c6c3..a7534add76e36 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js @@ -71,7 +71,7 @@ describe('Vislib Heatmap Chart Test Suite', function () { describe('for ' + name + ' Data', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'heatmap', addLegend: true, addTooltip: true, @@ -84,7 +84,7 @@ describe('Vislib Heatmap Chart Test Suite', function () { }; function generateVis(opts = {}) { - const config = _.defaultsDeep({}, opts, visLibParams); + const config = _.defaultsDeep({}, opts, vislibParams); vis = getVis(config); mockUiState = getMockUiState(); vis.on('brush', _.noop); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js index 64fbae7d1ac8c..983c15c004b97 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js @@ -42,8 +42,8 @@ const defaults = { * @param chartData {Object} Elasticsearch query results for this specific chart */ export class LineChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); + constructor(handler, chartEl, chartData, seriesConfigArgs, core) { + super(handler, chartEl, chartData, seriesConfigArgs, core); this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js index a84c74c095051..8d86df7c27da4 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js @@ -71,14 +71,14 @@ describe('Vislib Line Chart', function () { let mockUiState; beforeEach(() => { - const visLibParams = { + const vislibParams = { type: 'line', addLegend: true, addTooltip: true, drawLinesBetweenPoints: true, }; - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.render(data, mockUiState); vis.on('brush', _.noop); diff --git a/src/plugins/visualizations/public/components/_visualization.scss b/src/plugins/visualizations/public/components/_visualization.scss index f5e2d4fcf2862..bde9621fd70b8 100644 --- a/src/plugins/visualizations/public/components/_visualization.scss +++ b/src/plugins/visualizations/public/components/_visualization.scss @@ -70,10 +70,10 @@ flex-direction: column; } -.visChart__spinner { +.visChart__spinner, .visError { display: flex; flex: 1 1 auto; justify-content: center; align-items: center; + text-align: center; } - diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 081399fd1fbea..7bd4466b23166 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -52,6 +52,7 @@ export { ISavedVis, VisSavedObject, VisResponseValue, + VisToExpressionAst, } from './types'; export { ExprVisAPIEvents } from './expressions/vis'; export { VisualizationListItem } from './vis_types/vis_type_alias_registry'; diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index 959d9031853af..2c6cfc6fb7462 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -6,8 +6,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles pie function 1`] = `"kibana_pie visConfig='{\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"buckets\\":[1,2]}}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 501a69080d93f..0c210a04d2007 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -21,14 +21,12 @@ import { prepareJson, prepareString, buildPipelineVisFunction, - buildVislibDimensions, buildPipeline, SchemaConfig, Schemas, } from './build_pipeline'; import { Vis } from '..'; import { dataPluginMock } from '../../../../plugins/data/public/mocks'; -import { IndexPattern, IAggConfigs } from '../../../../plugins/data/public'; import { parseExpression } from '../../../expressions/common'; describe('visualize loader pipeline helpers: build pipeline', () => { @@ -136,15 +134,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const actual = buildPipelineVisFunction.tile_map(params, schemas, uiState); expect(actual).toMatchSnapshot(); }); - - it('handles pie function', () => { - const schemas = { - ...schemasDef, - segment: [1, 2], - }; - const actual = buildPipelineVisFunction.pie({}, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); }); describe('buildPipeline', () => { @@ -174,157 +163,4 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(expression).toMatchSnapshot(); }); }); - - describe('buildVislibDimensions', () => { - const dataStart = dataPluginMock.createStartContract(); - - let aggs: IAggConfigs; - let vis: Vis; - let params: any; - - beforeEach(() => { - aggs = dataStart.search.aggs.createAggConfigs({} as IndexPattern, [ - { - id: '0', - enabled: true, - type: 'count', - schema: 'metric', - params: {}, - }, - ]); - - params = { - searchSource: null, - timefilter: dataStart.query.timefilter.timefilter, - timeRange: null, - }; - }); - - describe('test y dimension format for histogram chart', () => { - beforeEach(() => { - vis = { - // @ts-ignore - type: { - name: 'histogram', - }, - params: { - seriesParams: [ - { - data: { id: '0' }, - valueAxis: 'axis-y', - }, - ], - valueAxes: [ - { - id: 'axis-y', - scale: { - mode: 'normal', - }, - }, - ], - }, - data: { - aggs, - searchSource: {} as any, - }, - isHierarchical: () => { - return false; - }, - }; - }); - - it('with one numeric metric in regular moder', async () => { - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'number' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - - it('with one numeric metric in percentage mode', async () => { - vis.params.valueAxes[0].scale.mode = 'percentage'; - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'percent' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - - it('with two numeric metrics, mixed normal and percent mode should have corresponding formatters', async () => { - aggs.createAggConfig({ - id: '5', - enabled: true, - type: 'count', - schema: 'metric', - params: {}, - }); - - vis.params = { - seriesParams: [ - { - data: { id: '0' }, - valueAxis: 'axis-y-1', - }, - { - data: { id: '5' }, - valueAxis: 'axis-y-2', - }, - ], - valueAxes: [ - { - id: 'axis-y-1', - scale: { - mode: 'normal', - }, - }, - { - id: 'axis-y-2', - scale: { - mode: 'percentage', - }, - }, - ], - }; - - const dimensions = await buildVislibDimensions(vis, params); - const expectedY1 = { id: 'number' }; - const expectedY2 = { id: 'percent' }; - expect(dimensions.y[0].format).toEqual(expectedY1); - expect(dimensions.y[1].format).toEqual(expectedY2); - }); - }); - - describe('test y dimension format for gauge chart', () => { - beforeEach(() => { - vis = { - // @ts-ignore - type: { - name: 'gauge', - }, - params: { gauge: {} }, - data: { - aggs, - searchSource: {} as any, - }, - isHierarchical: () => { - return false; - }, - }; - }); - - it('with percentageMode = false', async () => { - vis.params.gauge.percentageMode = false; - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'number' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - - it('with percentageMode = true', async () => { - vis.params.gauge.percentageMode = true; - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'percent' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - }); - }); }); diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index b08583c376b36..3593d62b9d2e6 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -17,8 +17,6 @@ * under the License. */ -import { get } from 'lodash'; -import moment from 'moment'; import { formatExpression, SerializedFieldFormat } from '../../../../plugins/expressions/public'; import { IAggConfig, search, TimefilterContract } from '../../../../plugins/data/public'; import { Vis, VisParams } from '../types'; @@ -76,16 +74,6 @@ export interface BuildPipelineParams { abortSignal?: AbortSignal; } -const vislibCharts: string[] = [ - 'area', - 'gauge', - 'goal', - 'heatmap', - 'histogram', - 'horizontal_bar', - 'line', -]; - export const getSchemas = ( vis: Vis, { timeRange, timefilter }: BuildPipelineParams @@ -230,29 +218,6 @@ export const prepareDimension = (variable: string, data: any) => { return expr; }; -const adjustVislibDimensionFormmaters = (vis: Vis, dimensions: { y: any[] }): void => { - const visConfig = vis.params; - const responseAggs = vis.data.aggs!.getResponseAggs().filter((agg: IAggConfig) => agg.enabled); - - (dimensions.y || []).forEach((yDimension) => { - const yAgg = responseAggs[yDimension.accessor]; - const seriesParam = (visConfig.seriesParams || []).find( - (param: any) => param.data.id === yAgg.id - ); - if (seriesParam) { - const usedValueAxis = (visConfig.valueAxes || []).find( - (valueAxis: any) => valueAxis.id === seriesParam.valueAxis - ); - if (get(usedValueAxis, 'scale.mode') === 'percentage') { - yDimension.format = { id: 'percent' }; - } - } - if (get(visConfig, 'gauge.percentageMode') === true) { - yDimension.format = { id: 'percent' }; - } - }); -}; - export const buildPipelineVisFunction: BuildPipelineVisFunction = { input_control_vis: (params) => { return `input_control_vis ${prepareJson('visConfig', params)}`; @@ -278,13 +243,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { }; return `tilemap ${prepareJson('visConfig', visConfig)}`; }, - pie: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.pie(schemas), - }; - return `kibana_pie ${prepareJson('visConfig', visConfig)}`; - }, }; const buildVisConfig: BuildVisConfigFunction = { @@ -305,55 +263,6 @@ const buildVisConfig: BuildVisConfigFunction = { }; return visConfig; }, - pie: (schemas) => { - const visConfig = {} as any; - visConfig.dimensions = { - metric: schemas.metric[0], - buckets: schemas.segment, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - return visConfig; - }, -}; - -export const buildVislibDimensions = async (vis: any, params: BuildPipelineParams) => { - const schemas = getSchemas(vis, { - timeRange: params.timeRange, - timefilter: params.timefilter, - }); - const dimensions = { - x: schemas.segment ? schemas.segment[0] : null, - y: schemas.metric, - z: schemas.radius, - width: schemas.width, - series: schemas.group, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - if (schemas.segment) { - const xAgg = vis.data.aggs.getResponseAggs()[dimensions.x.accessor]; - if (xAgg.type.name === 'date_histogram') { - dimensions.x.params.date = true; - const { esUnit, esValue } = xAgg.buckets.getInterval(); - dimensions.x.params.interval = moment.duration(esValue, esUnit); - dimensions.x.params.intervalESValue = esValue; - dimensions.x.params.intervalESUnit = esUnit; - dimensions.x.params.format = xAgg.buckets.getScaledDateFormat(); - dimensions.x.params.bounds = xAgg.buckets.getBounds(); - } else if (xAgg.type.name === 'histogram') { - const intervalParam = xAgg.type.paramByName('interval'); - const output = { params: {} as any }; - await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { - abortSignal: params.abortSignal, - }); - intervalParam.write(xAgg, output); - dimensions.x.params.interval = output.params.interval; - } - } - - adjustVislibDimensionFormmaters(vis, dimensions); - return dimensions; }; export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { @@ -396,11 +305,6 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { schemas, uiState ); - } else if (vislibCharts.includes(vis.type.name)) { - const visConfig = { ...vis.params }; - visConfig.dimensions = await buildVislibDimensions(vis, params); - - pipeline += `vislib type='${vis.type.name}' ${prepareJson('visConfig', visConfig)}`; } else { const visConfig = { ...vis.params }; visConfig.dimensions = schemas; diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 68ab3561d375c..3c322f5e79165 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -76,4 +76,4 @@ export interface VisToExpressionAstParams { export type VisToExpressionAst = ( vis: Vis, params: VisToExpressionAstParams -) => ExpressionAstExpression; +) => Promise | ExpressionAstExpression; diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index cb114866322db..1acea624ad4cd 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -218,7 +218,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr } public async expectError() { - await testSubjects.existOrFail('visLibVisualizeError'); + await testSubjects.existOrFail('vislibVisualizeError'); } public async getVisualizationRenderingCount() { From fe807d68a6dabb735e1488c66fda6de8675f2f8b Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 29 Oct 2020 15:10:18 -0400 Subject: [PATCH 45/73] [Maps] Add readme for Maps plugins (#82023) --- .github/CODEOWNERS | 2 -- docs/developer/plugin-list.asciidoc | 16 ++++++++-------- src/plugins/maps_legacy/README.md | 7 +++++++ src/plugins/region_map/README.md | 5 +++++ src/plugins/tile_map/README.md | 5 +++++ x-pack/plugins/file_upload/README.md | 3 +++ 6 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 src/plugins/maps_legacy/README.md create mode 100644 src/plugins/region_map/README.md create mode 100644 src/plugins/tile_map/README.md create mode 100644 x-pack/plugins/file_upload/README.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 525da9d832b53..28380549c751c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -171,8 +171,6 @@ /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis -#CC# /src/legacy/core_plugins/region_map @elastic/kibana-gis -#CC# /src/legacy/core_plugins/tile_map @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis #CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index f30afce3ee02c..dee6e4777884c 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -134,8 +134,8 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |WARNING: Missing README. -|{kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/maps_legacy/README.md[mapsLegacy] +|Internal objects used by the Coordinate, Region, and Vega visualizations. |{kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] @@ -147,8 +147,8 @@ It also provides a stateful version of it on the start contract. |WARNING: Missing README. -|{kib-repo}blob/{branch}/src/plugins/region_map[regionMap] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/region_map/README.md[regionMap] +|Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. |{kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] @@ -180,8 +180,8 @@ so they can properly protect the data within their clusters. |This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry). -|{kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/tile_map/README.md[tileMap] +|Create a coordinate map. Display the results of a geohash_tile aggregation as bubbles, rectangles, or heatmap color blobs. |{kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] @@ -355,8 +355,8 @@ and actions. |WARNING: Missing README. -|{kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/file_upload/README.md[fileUpload] +|Backend and core front-end react-components for GeoJson file upload. Only supports the Maps plugin. |{kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] diff --git a/src/plugins/maps_legacy/README.md b/src/plugins/maps_legacy/README.md new file mode 100644 index 0000000000000..4a870e4f7492d --- /dev/null +++ b/src/plugins/maps_legacy/README.md @@ -0,0 +1,7 @@ +# Maps legacy + +Internal objects used by the Coordinate, Region, and Vega visualizations. + +It exports the default Leaflet-based map and exposes the connection to the Elastic Maps service. + +This plugin is targeted for removal in 8.0. \ No newline at end of file diff --git a/src/plugins/region_map/README.md b/src/plugins/region_map/README.md new file mode 100644 index 0000000000000..540ab47c102d3 --- /dev/null +++ b/src/plugins/region_map/README.md @@ -0,0 +1,5 @@ +# Region map visualization + +Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. + +This plugin is targeted for removal in 8.0. \ No newline at end of file diff --git a/src/plugins/tile_map/README.md b/src/plugins/tile_map/README.md new file mode 100644 index 0000000000000..633ee7dba46d6 --- /dev/null +++ b/src/plugins/tile_map/README.md @@ -0,0 +1,5 @@ +# Coordinate map visualization + +Create a coordinate map. Display the results of a geohash_tile aggregation as bubbles, rectangles, or heatmap color blobs. + +This plugin is targeted for removal in 8.0. \ No newline at end of file diff --git a/x-pack/plugins/file_upload/README.md b/x-pack/plugins/file_upload/README.md new file mode 100644 index 0000000000000..0d4b4da61ccf6 --- /dev/null +++ b/x-pack/plugins/file_upload/README.md @@ -0,0 +1,3 @@ +# File upload + +Backend and core front-end react-components for GeoJson file upload. Only supports the Maps plugin. \ No newline at end of file From 915997e08b3c98df5afe839f19810b89f8e77db1 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 29 Oct 2020 14:37:07 -0500 Subject: [PATCH 46/73] skip 'should timeout if payload sending has too long of an idle period' --- src/core/server/http/integration_tests/router.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index e19c348511f1a..ad228d9b0bb9d 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -384,7 +384,7 @@ describe('Options', () => { }); describe('idleSocket', () => { - it('should timeout if payload sending has too long of an idle period', async () => { + it.skip('should timeout if payload sending has too long of an idle period', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); From ca028b79aab40a2a432b5fcd23af9581f5edf8b2 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Thu, 29 Oct 2020 19:45:37 +0000 Subject: [PATCH 47/73] Add sort order to getJourneySteps query (#82038) --- x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index e4392480f5b72..d0bb67795d46c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -35,6 +35,7 @@ export const getJourneySteps: UMElasticsearchQueryFn Date: Thu, 29 Oct 2020 15:14:59 -0500 Subject: [PATCH 48/73] [Search] Support non-shard error information in ES responses (#81967) * Display top-level error reason if no shard info is available For EQL queries, error responses do not contain `failed_shards` information, and so our error toasts contained only a stack trace. With this addition, we'll fall back to the top-level `error.reason` if those fields are not present, giving the user better indication of what's going on without having to inspect the actual network response. * Prevent service from changing the shape of our errors This ensures a consistent interface for our kibana client errors, whether the error is raised directly from a `fetch` or emitted from a search strategy's observable; namely: that both contain this top-level `body` key. * Make our body property public This is the same visibility as the original error: if it were protected it wouldn't be very useful to consumers. * Adds a unit test for the interface adherence --- .../public/search/errors/es_error.test.tsx | 40 +++++++++++++++++++ .../data/public/search/errors/es_error.tsx | 11 +++-- .../data/public/search/errors/utils.ts | 4 ++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/plugins/data/public/search/errors/es_error.test.tsx diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx new file mode 100644 index 0000000000000..db719eb6a70c9 --- /dev/null +++ b/src/plugins/data/public/search/errors/es_error.test.tsx @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EsError } from './es_error'; +import { IEsError } from './types'; + +describe('EsError', () => { + it('contains the same body as the wrapped error', () => { + const error = { + body: { + attributes: { + error: { + type: 'top_level_exception_type', + reason: 'top-level reason', + }, + }, + }, + } as IEsError; + const esError = new EsError(error); + + expect(typeof esError.body).toEqual('object'); + expect(esError.body).toEqual(error.body); + }); +}); diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx index 53d00159b836b..f1e7eaa707a06 100644 --- a/src/plugins/data/public/search/errors/es_error.tsx +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -22,22 +22,27 @@ import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { ApplicationStart } from 'kibana/public'; import { KbnError } from '../../../../kibana_utils/common'; import { IEsError } from './types'; -import { getRootCause } from './utils'; +import { getRootCause, getTopLevelCause } from './utils'; export class EsError extends KbnError { + readonly body: IEsError['body']; + constructor(protected readonly err: IEsError) { super('EsError'); + this.body = err.body; } public getErrorMessage(application: ApplicationStart) { const rootCause = getRootCause(this.err)?.reason; + const topLevelCause = getTopLevelCause(this.err)?.reason; + const cause = rootCause ?? topLevelCause; return ( <> - {rootCause ? ( + {cause ? ( - {rootCause} + {cause} ) : null} diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts index d07d9b05e91e9..45a318904665f 100644 --- a/src/plugins/data/public/search/errors/utils.ts +++ b/src/plugins/data/public/search/errors/utils.ts @@ -26,6 +26,10 @@ export function getFailedShards(err: IEsError) { return failedShards ? failedShards[0] : undefined; } +export function getTopLevelCause(err: IEsError) { + return err.body?.attributes?.error; +} + export function getRootCause(err: IEsError) { return getFailedShards(err)?.reason; } From 0094f7ea8e2ce989fdd0c9687a925da1da629dc0 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 29 Oct 2020 13:26:51 -0700 Subject: [PATCH 49/73] [Fleet] Add experimental copy to upgrade agent(s) (#81410) * Add experimental copy to upgrade agent(s) * Adjust copy after review * Fix string * Adjust quotes --- .../components/agent_upgrade_modal/index.tsx | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_upgrade_modal/index.tsx index cde54678b2d2f..43ad7208c3d81 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_upgrade_modal/index.tsx @@ -5,7 +5,13 @@ */ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { + EuiConfirmModal, + EuiOverlayMask, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { sendPostAgentUpgrade, sendPostBulkAgentUpgrade, useCore } from '../../../../hooks'; @@ -65,18 +71,38 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ - ) : ( - - ) + + + {isSingleAgent ? ( + + ) : ( + + )} + + + + } + tooltipContent={ + + } + /> + + } onCancel={onClose} onConfirm={onSubmit} @@ -106,7 +132,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ {isSingleAgent ? ( = ({ ) : ( )} From 2a3846181740cadab56935cf2df23d5d809e6526 Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Thu, 29 Oct 2020 16:52:02 -0400 Subject: [PATCH 50/73] [Lens] Adjust Lens Visualization Padding in Dashboards (#81911) * reduce padding on lens visualizations in dashboard * tweak padding and axes title colors to match lens * remove faux padding (border) to match lens padding * update snapshots * Revert "update snapshots" This reverts commit c63cf2bf1c023aa0b6325f19af01940b066915f3. * update functional test baseline screenshot --- .../components/vis_types/_vis_types.scss | 14 ++++---------- .../components/vis_types/timeseries/vis.js | 10 +--------- .../public/vislib/lib/layout/_layout.scss | 6 ++---- .../screenshots/baseline/tsvb_dashboard.png | Bin 118701 -> 123348 bytes .../embeddable/expression_wrapper.tsx | 2 +- 5 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss index c445d456a1703..9585711c73dd2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss @@ -3,29 +3,23 @@ flex-direction: column; flex: 1 1 100%; - // border used in lieu of padding to prevent overlapping background-color - border-width: $euiSizeS; - border-style: solid; - border-color: transparent; - .tvbVisTimeSeries { overflow: hidden; } .tvbVisTimeSeriesDark { .echReactiveChart_unavailable { - color: #DFE5EF; + color: #dfe5ef; } - .echLegendItem { - color: #DFE5EF; + .echLegendItem { + color: #dfe5ef; } } .tvbVisTimeSeriesLight { .echReactiveChart_unavailable { color: #343741; } - .echLegendItem { + .echLegendItem { color: #343741; } } } - diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 2434285bd94c6..c12e518a9dcd3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -19,7 +19,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import reactCSS from 'reactcss'; import { startsWith, get, cloneDeep, map } from 'lodash'; import { htmlIdGenerator } from '@elastic/eui'; @@ -150,13 +149,6 @@ export class TimeseriesVisualization extends Component { render() { const { model, visData, onBrush } = this.props; - const styles = reactCSS({ - default: { - tvbVis: { - borderColor: get(model, 'background_color'), - }, - }, - }); const series = get(visData, `${model.id}.series`, []); const interval = getInterval(visData, model); const yAxisIdGenerator = htmlIdGenerator('yaxis'); @@ -231,7 +223,7 @@ export class TimeseriesVisualization extends Component { }); return ( -
+
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 acV8gw`Q#-xQfNB;*e^Y4uS literal 118701 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%0AbQPN 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 Date: Thu, 29 Oct 2020 13:55:50 -0700 Subject: [PATCH 51/73] [Metrics UI] Add endpoint for Metrics API (#81693) * [Metrics UI] Add endpoint for Metrics API * Adding the ability to caculate the interval based on a module * fixing types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../infra/common/http_api/metrics_api.ts | 1 + x-pack/plugins/infra/server/infra_server.ts | 2 + .../plugins/infra/server/lib/metrics/index.ts | 14 +++++- .../lib/metrics/lib/calculate_interval.ts | 34 +++++++++++++ .../infra/server/routes/metrics_api/index.ts | 50 +++++++++++++++++++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_api/index.ts diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts index 41657fdce2153..14b7c24e57694 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_api.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -30,6 +30,7 @@ export const MetricsAPIRequestRT = rt.intersection([ }), rt.partial({ groupBy: rt.array(groupByRT), + modules: rt.array(rt.string), afterKey: rt.union([rt.null, afterKeyObjectRT]), limit: rt.union([rt.number, rt.null, rt.undefined]), filters: rt.array(rt.object), diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 1d89b7be43296..49fe55e3dee01 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -25,6 +25,7 @@ import { import { initGetK8sAnomaliesRoute } from './routes/infra_ml'; import { initGetHostsAnomaliesRoute } from './routes/infra_ml'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; +import { initMetricsAPIRoute } from './routes/metrics_api'; import { initMetadataRoute } from './routes/metadata'; import { initSnapshotRoute } from './routes/snapshot'; import { initNodeDetailsRoute } from './routes/node_details'; @@ -74,6 +75,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogEntriesSummaryHighlightsRoute(libs); initLogEntriesItemRoute(libs); initMetricExplorerRoute(libs); + initMetricsAPIRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); initLogSourceConfigurationRoutes(libs); diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index 183254a0486a2..9401d34ca62fc 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -17,11 +17,20 @@ import { EMPTY_RESPONSE } from './constants'; import { createAggregations } from './lib/create_aggregations'; import { convertHistogramBucketsToTimeseries } from './lib/convert_histogram_buckets_to_timeseries'; import { calculateBucketSize } from './lib/calculate_bucket_size'; +import { calculatedInterval } from './lib/calculate_interval'; export const query = async ( search: ESSearchClient, - options: MetricsAPIRequest + rawOptions: MetricsAPIRequest ): Promise => { + const interval = await calculatedInterval(search, rawOptions); + const options = { + ...rawOptions, + timerange: { + ...rawOptions.timerange, + interval, + }, + }; const hasGroupBy = Array.isArray(options.groupBy) && options.groupBy.length > 0; const filter: Array> = [ { @@ -35,6 +44,7 @@ export const query = async ( }, ...(options.groupBy?.map((field) => ({ exists: { field } })) ?? []), ]; + const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -70,7 +80,7 @@ export const query = async ( throw new Error('Aggregations should be present.'); } - const { bucketSize } = calculateBucketSize(options.timerange); + const { bucketSize } = calculateBucketSize({ ...options.timerange, interval }); if (hasGroupBy && GroupingResponseRT.is(response.aggregations)) { const { groupings } = response.aggregations; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts new file mode 100644 index 0000000000000..46682e2213a3c --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isArray, isNumber } from 'lodash'; +import { MetricsAPIRequest } from '../../../../common/http_api'; +import { ESSearchClient } from '../types'; +import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; + +export const calculatedInterval = async (search: ESSearchClient, options: MetricsAPIRequest) => { + const useModuleInterval = + options.timerange.interval === 'modules' && + isArray(options.modules) && + options.modules.length > 0; + + const calcualatedInterval = useModuleInterval + ? await calculateMetricInterval( + search, + { + indexPattern: options.indexPattern, + timestampField: options.timerange.field, + timerange: { from: options.timerange.from, to: options.timerange.to }, + }, + options.modules + ) + : false; + + const defaultInterval = + options.timerange.interval === 'modules' ? 'auto' : options.timerange.interval; + + return isNumber(calcualatedInterval) ? `>=${calcualatedInterval}s` : defaultInterval; +}; diff --git a/x-pack/plugins/infra/server/routes/metrics_api/index.ts b/x-pack/plugins/infra/server/routes/metrics_api/index.ts new file mode 100644 index 0000000000000..f3dcdeeb70cc1 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_api/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { throwErrors } from '../../../common/runtime_types'; +import { createSearchClient } from '../../lib/create_search_client'; +import { query } from '../../lib/metrics'; +import { MetricsAPIRequestRT, MetricsAPIResponseRT } from '../../../common/http_api'; + +const escapeHatch = schema.object({}, { unknowns: 'allow' }); + +export const initMetricsAPIRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metrics_api', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { + try { + const options = pipe( + MetricsAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const client = createSearchClient(requestContext, framework); + const metricsApiResponse = await query(client, options); + + return response.ok({ + body: MetricsAPIResponseRT.encode(metricsApiResponse), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); +}; From dc56b562013bea5b103dbd0e20ebeeebedcc1595 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 29 Oct 2020 17:01:33 -0600 Subject: [PATCH 52/73] [docs] Add missing App Arch READMEs. (#82080) --- docs/developer/plugin-list.asciidoc | 4 ++-- examples/bfetch_explorer/README.md | 11 +++++++++ examples/embeddable_examples/README.md | 4 ++++ examples/embeddable_explorer/README.md | 10 ++++++++ examples/state_containers_examples/README.md | 8 +++++++ x-pack/plugins/data_enhanced/README.md | 25 ++++++++++++++++++++ 6 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 examples/bfetch_explorer/README.md create mode 100644 examples/embeddable_examples/README.md create mode 100644 examples/embeddable_explorer/README.md create mode 100644 examples/state_containers_examples/README.md create mode 100644 x-pack/plugins/data_enhanced/README.md diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index dee6e4777884c..4387e168f412d 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -323,8 +323,8 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error |The deprecated dashboard only mode. -|{kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/data_enhanced/README.md[dataEnhanced] +|The data_enhanced plugin is the x-pack counterpart to the OSS data plugin. |{kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] diff --git a/examples/bfetch_explorer/README.md b/examples/bfetch_explorer/README.md new file mode 100644 index 0000000000000..33723e7cabe07 --- /dev/null +++ b/examples/bfetch_explorer/README.md @@ -0,0 +1,11 @@ +## bfetch explorer + +bfetch is a service that allows you to batch HTTP requests and stream responses +back. + +This example app demonstrates: + - How you can create a streaming response route and consume it from the + client + - How you can create a batch processing route and consume it from the client + +To run this example, use the command `yarn start --run-examples`. diff --git a/examples/embeddable_examples/README.md b/examples/embeddable_examples/README.md new file mode 100644 index 0000000000000..d05c315d31817 --- /dev/null +++ b/examples/embeddable_examples/README.md @@ -0,0 +1,4 @@ +## Embeddable examples + +This example plugin exists to support the `embeddable_explorer` app. + diff --git a/examples/embeddable_explorer/README.md b/examples/embeddable_explorer/README.md new file mode 100644 index 0000000000000..0425790f07487 --- /dev/null +++ b/examples/embeddable_explorer/README.md @@ -0,0 +1,10 @@ +## Embeddable explorer + +This example app shows how to: + - Create a basic "hello world" embeddable + - Create embeddables that accept inputs and use an EmbeddableRenderer + - Nest embeddables inside a container + - Dynamically add children to embeddable containers + - Work with the EmbeddablePanel component + +To run this example, use the command `yarn start --run-examples`. diff --git a/examples/state_containers_examples/README.md b/examples/state_containers_examples/README.md new file mode 100644 index 0000000000000..c4c6642789bd9 --- /dev/null +++ b/examples/state_containers_examples/README.md @@ -0,0 +1,8 @@ +## State containers examples + +This example app shows how to: + - Use state containers to manage your application state + - Integrate with browser history and hash history routing + - Sync your state container with the URL + +To run this example, use the command `yarn start --run-examples`. diff --git a/x-pack/plugins/data_enhanced/README.md b/x-pack/plugins/data_enhanced/README.md new file mode 100644 index 0000000000000..8f3ae7ac3cd13 --- /dev/null +++ b/x-pack/plugins/data_enhanced/README.md @@ -0,0 +1,25 @@ +# data_enhanced + +The `data_enhanced` plugin is the x-pack counterpart to the OSS `data` plugin. + +It exists to provide Elastic-licensed services, or parts of services, which +enhance existing OSS functionality from `data`. + +Currently the `data_enhanced` plugin doesn't return any APIs which you can +consume directly, however it is possible that you are indirectly relying on the +enhanced functionality that it provides via the OSS `data` plugin. + +Here is the functionality it adds: + +## KQL Autocomplete + +The OSS autocomplete service provides suggestions for field names and values +based on suggestion providers which are registered to the service. This plugin +registers the autocomplete provider for KQL to the OSS service. + +## Async, Rollup, and EQL Search Strategies + +This plugin enhances the OSS search service with an ES search strategy that +uses async search (or rollups) behind the scenes. It also registers an EQL +search strategy. + From f095ec366343b5efe10f9621fd9f47483f539b63 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 29 Oct 2020 16:16:32 -0700 Subject: [PATCH 53/73] Closes #80629, with proper timeout messaging and docs for user to work around the scalability issue. (#82083) --- docs/settings/apm-settings.asciidoc | 6 +++ x-pack/plugins/apm/common/service_map.ts | 2 + .../components/app/ServiceMap/index.tsx | 18 ++++++- .../app/ServiceMap/timeout_prompt.tsx | 53 +++++++++++++++++++ .../plugins/apm/public/hooks/useFetcher.tsx | 2 +- .../lib/service_map/get_trace_sample_ids.ts | 46 +++++++++------- 6 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/timeout_prompt.tsx diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 9054a97c90496..aa680720fc8ff 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -43,6 +43,12 @@ Changing these settings may disable features of the APM App. | `xpack.apm.enabled` | Set to `false` to disable the APM app. Defaults to `true`. +| `xpack.apm.serviceMapFingerprintBucketSize` + | Maximum number of unique transaction combinations sampled for generating service map focused on a specific service. Defaults to `100`. + +| `xpack.apm.serviceMapFingerprintGlobalBucketSize` + | Maximum number of unique transaction combinations sampled for generating the global service map. Defaults to `100`. + | `xpack.apm.ui.enabled` {ess-icon} | Set to `false` to hide the APM app from the main menu. Defaults to `true`. diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 02456f9b2050f..6edf56fb9a1ae 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -91,3 +91,5 @@ export function isSpanGroupingSupported(type?: string, subtype?: string) { nongroupedSubType === 'all' || nongroupedSubType === subtype ); } + +export const SERVICE_MAP_TIMEOUT_ERROR = 'ServiceMapTimeoutError'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index d167b6a9a0565..752f9b7fda243 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -10,6 +10,7 @@ import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, + SERVICE_MAP_TIMEOUT_ERROR, } from '../../../../common/service_map'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; @@ -22,6 +23,7 @@ import { Cytoscape } from './Cytoscape'; import { getCytoscapeDivStyle } from './cytoscape_options'; import { EmptyBanner } from './EmptyBanner'; import { EmptyPrompt } from './empty_prompt'; +import { TimeoutPrompt } from './timeout_prompt'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; @@ -61,7 +63,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { const license = useLicense(); const { urlParams } = useUrlParams(); - const { data = { elements: [] }, status } = useFetcher(() => { + const { data = { elements: [] }, status, error } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. if (!license || !isActivePlatinumLicense(license)) { return; @@ -109,6 +111,20 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { ); } + if ( + status === FETCH_STATUS.FAILURE && + error && + 'body' in error && + error.body.statusCode === 500 && + error.body.message === SERVICE_MAP_TIMEOUT_ERROR + ) { + return ( + + + + ); + } + return (
+ {i18n.translate('xpack.apm.serviceMap.timeoutPromptTitle', { + defaultMessage: 'Service map timeout', + })} + + } + body={ +

+ {i18n.translate('xpack.apm.serviceMap.timeoutPromptDescription', { + defaultMessage: `Timed out while fetching data for service map. Limit the scope by selecting a smaller time range, or use configuration setting '{configName}' with a reduced value.`, + values: { + configName: isGlobalServiceMap + ? 'xpack.apm.serviceMapFingerprintGlobalBucketSize' + : 'xpack.apm.serviceMapFingerprintBucketSize', + }, + })} +

+ } + actions={} + /> + ); +} + +function ApmSettingsDocLink() { + return ( + + {i18n.translate('xpack.apm.serviceMap.timeoutPrompt.docsLink', { + defaultMessage: 'Learn more about APM settings in the docs', + })} + + ); +} diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.tsx index 5d65424844c5a..6add0e8a2b480 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/useFetcher.tsx @@ -21,7 +21,7 @@ export enum FETCH_STATUS { export interface FetcherResult { data?: Data; status: FETCH_STATUS; - error?: Error; + error?: IHttpFetchError; } // fetcher functions can return undefined OR a promise. Previously we had a more simple type diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index dfc4e02c25a7f..524b9bfdc7891 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq, take, sortBy } from 'lodash'; +import Boom from 'boom'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -15,6 +16,7 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../common/elasticsearch_fieldnames'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map'; const MAX_TRACES_TO_INSPECT = 1000; @@ -122,26 +124,30 @@ export async function getTraceSampleIds({ }, }; - const tracesSampleResponse = await apmEventClient.search(params); + try { + const tracesSampleResponse = await apmEventClient.search(params); + // make sure at least one trace per composite/connection bucket + // is queried + const traceIdsWithPriority = + tracesSampleResponse.aggregations?.connections.buckets.flatMap((bucket) => + bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({ + traceId: sampleDocBucket.key as string, + priority: index, + })) + ) || []; - // make sure at least one trace per composite/connection bucket - // is queried - const traceIdsWithPriority = - tracesSampleResponse.aggregations?.connections.buckets.flatMap((bucket) => - bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({ - traceId: sampleDocBucket.key as string, - priority: index, - })) - ) || []; + const traceIds = take( + uniq( + sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId) + ), + MAX_TRACES_TO_INSPECT + ); - const traceIds = take( - uniq( - sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId) - ), - MAX_TRACES_TO_INSPECT - ); - - return { - traceIds, - }; + return { traceIds }; + } catch (error) { + if ('displayName' in error && error.displayName === 'RequestTimeout') { + throw Boom.internal(SERVICE_MAP_TIMEOUT_ERROR); + } + throw error; + } } From 4f717708b415a86aca8c56d06a8f43ea998ec834 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Fri, 30 Oct 2020 05:34:20 +0300 Subject: [PATCH 54/73] [Telemetry] [Schema] remove number type and support all es number types (#81774) --- packages/kbn-telemetry-tools/GUIDELINE.md | 9 +-------- .../src/tools/__fixture__/mock_schema.json | 12 ++++++------ ...dexed_interface_with_not_matching_schema.ts | 2 +- ...ed_schema_defined_with_spreads_collector.ts | 2 +- .../__fixture__/parsed_working_collector.ts | 12 ++++++------ .../extract_collectors.test.ts.snap | 14 +++++++------- .../tools/check_collector__integrity.test.ts | 6 +++--- .../src/tools/manage_schema.ts | 18 ++++++++---------- .../schema_defined_with_spreads_collector.ts | 2 +- .../telemetry_collectors/working_collector.ts | 12 ++++++------ .../data/server/search/collectors/register.ts | 6 +++--- src/plugins/telemetry/schema/oss_plugins.json | 6 +++--- src/plugins/usage_collection/README.md | 6 +++--- .../server/collector/collector.ts | 11 +++-------- .../usage_collection/server/collector/index.ts | 1 + .../security_usage_collector.ts | 2 +- .../schema/xpack_plugins.json | 2 +- 17 files changed, 55 insertions(+), 68 deletions(-) diff --git a/packages/kbn-telemetry-tools/GUIDELINE.md b/packages/kbn-telemetry-tools/GUIDELINE.md index e7d09babbf9e2..a22196bb5dc74 100644 --- a/packages/kbn-telemetry-tools/GUIDELINE.md +++ b/packages/kbn-telemetry-tools/GUIDELINE.md @@ -148,14 +148,7 @@ usageCollection.makeUsageCollector({ Any field property in the schema accepts a `type` field. By default the type is `object` which accepts nested properties under it. Currently we accept the following property types: ``` -AllowedSchemaTypes = - | 'keyword' - | 'text' - | 'number' - | 'boolean' - | 'long' - | 'date' - | 'float'; +'long', 'integer', 'short', 'byte', 'double', 'float', 'keyword', 'text', 'boolean', 'date' ``` diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json index 51e5df9bf7dc0..a385cd6798365 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json @@ -8,16 +8,16 @@ "my_index_signature_prop": { "properties": { "avg": { - "type": "number" + "type": "float" }, "count": { - "type": "number" + "type": "long" }, "max": { - "type": "number" + "type": "long" }, "min": { - "type": "number" + "type": "long" } } }, @@ -27,7 +27,7 @@ "my_objects": { "properties": { "total": { - "type": "number" + "type": "long" }, "type": { "type": "boolean" @@ -39,7 +39,7 @@ "items": { "properties": { "total": { - "type": "number" + "type": "long" }, "type": { "type": "boolean" diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts index 83866a2b6afec..109fc045b6ee0 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts @@ -28,7 +28,7 @@ export const parsedIndexedInterfaceWithNoMatchingSchema: ParsedUsageCollection = value: { something: { count_1: { - type: 'number', + type: 'long', }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts index 833344fa368b0..4a1a622e23f36 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts @@ -34,7 +34,7 @@ export const parsedSchemaDefinedWithSpreadsCollector: ParsedUsageCollection = [ }, my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean', diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index acf984b7d10ee..ef6227cf35c37 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -34,21 +34,21 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ }, my_index_signature_prop: { avg: { - type: 'number', + type: 'float', }, count: { - type: 'number', + type: 'long', }, max: { - type: 'number', + type: 'long', }, min: { - type: 'number', + type: 'long', }, }, my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean', @@ -58,7 +58,7 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ type: 'array', items: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index 4725be77533af..fe589be7993d0 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -176,7 +176,7 @@ Array [ }, "my_objects": Object { "total": Object { - "type": "number", + "type": "long", }, "type": Object { "type": "boolean", @@ -248,7 +248,7 @@ Array [ "my_array": Object { "items": Object { "total": Object { - "type": "number", + "type": "long", }, "type": Object { "type": "boolean", @@ -258,21 +258,21 @@ Array [ }, "my_index_signature_prop": Object { "avg": Object { - "type": "number", + "type": "float", }, "count": Object { - "type": "number", + "type": "long", }, "max": Object { - "type": "number", + "type": "long", }, "min": Object { - "type": "number", + "type": "long", }, }, "my_objects": Object { "total": Object { - "type": "number", + "type": "long", }, "type": Object { "type": "boolean", diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts index a101210185a63..b6ea9d49cf6d0 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts @@ -44,7 +44,7 @@ describe('checkMatchingMapping', () => { it('returns diff on mismatching parsedCollections and stored mapping', async () => { const mockSchema = await parseJsonFile('mock_schema.json'); const malformedParsedCollector = cloneDeep(parsedWorkingCollector); - const fieldMapping = { type: 'number' }; + const fieldMapping = { type: 'long' }; malformedParsedCollector[1].schema.value.flat = fieldMapping; const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema); @@ -61,9 +61,9 @@ describe('checkMatchingMapping', () => { const mockSchema = await parseJsonFile('mock_schema.json'); const malformedParsedCollector = cloneDeep(parsedWorkingCollector); const collectorName = 'New Collector in town!'; - const collectorMapping = { some_usage: { type: 'number' } }; + const collectorMapping = { some_usage: { type: 'long' } }; malformedParsedCollector[1].collectorName = collectorName; - malformedParsedCollector[1].schema.value = { some_usage: { type: 'number' } }; + malformedParsedCollector[1].schema.value = { some_usage: { type: 'long' } }; const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema); expect(diffs).toEqual({ diff --git a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts index 7721492fdb691..e2bfca34a6487 100644 --- a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts +++ b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts @@ -19,14 +19,9 @@ import { ParsedUsageCollection } from './ts_parser'; -export type AllowedSchemaTypes = - | 'keyword' - | 'text' - | 'number' - | 'boolean' - | 'long' - | 'date' - | 'float'; +export type AllowedSchemaNumberTypes = 'long' | 'integer' | 'short' | 'byte' | 'double' | 'float'; + +export type AllowedSchemaTypes = AllowedSchemaNumberTypes | 'keyword' | 'text' | 'boolean' | 'date'; export function compatibleSchemaTypes(type: AllowedSchemaTypes | 'array') { switch (type) { @@ -36,9 +31,12 @@ export function compatibleSchemaTypes(type: AllowedSchemaTypes | 'array') { return 'string'; case 'boolean': return 'boolean'; - case 'number': - case 'float': case 'long': + case 'integer': + case 'short': + case 'byte': + case 'double': + case 'float': return 'number'; case 'array': return 'array'; diff --git a/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts b/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts index af9fef0bbd297..791c366199d74 100644 --- a/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts +++ b/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts @@ -49,7 +49,7 @@ const someSchema: MakeSchemaFrom> = { const someOtherSchema: MakeSchemaFrom> = { my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts index 0a3bf49638a7b..f9cb3bb568673 100644 --- a/src/fixtures/telemetry_collectors/working_collector.ts +++ b/src/fixtures/telemetry_collectors/working_collector.ts @@ -85,7 +85,7 @@ export const myCollector = makeUsageCollector({ }, my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, @@ -93,17 +93,17 @@ export const myCollector = makeUsageCollector({ type: 'array', items: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, }, my_str_array: { type: 'array', items: { type: 'keyword' } }, my_index_signature_prop: { - count: { type: 'number' }, - avg: { type: 'number' }, - max: { type: 'number' }, - min: { type: 'number' }, + count: { type: 'long' }, + avg: { type: 'float' }, + max: { type: 'long' }, + min: { type: 'long' }, }, }, }); diff --git a/src/plugins/data/server/search/collectors/register.ts b/src/plugins/data/server/search/collectors/register.ts index ab0ea93edd49e..5db4f52169350 100644 --- a/src/plugins/data/server/search/collectors/register.ts +++ b/src/plugins/data/server/search/collectors/register.ts @@ -37,9 +37,9 @@ export async function registerUsageCollector( isReady: () => true, fetch: fetchProvider(context.config.legacy.globalConfig$), schema: { - successCount: { type: 'number' }, - errorCount: { type: 'number' }, - averageDuration: { type: 'long' }, + successCount: { type: 'long' }, + errorCount: { type: 'long' }, + averageDuration: { type: 'float' }, }, }); usageCollection.registerCollector(collector); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 160e99a40790c..c840cbe8fc94d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -16,13 +16,13 @@ "search": { "properties": { "successCount": { - "type": "number" + "type": "long" }, "errorCount": { - "type": "number" + "type": "long" }, "averageDuration": { - "type": "long" + "type": "float" } } }, diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 430241cbe0a05..5a853972d34a8 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -138,7 +138,7 @@ The `schema` field is a proscribed data model assists with detecting changes in The `AllowedSchemaTypes` is the list of allowed schema types for the usage fields getting reported: ``` -'keyword', 'text', 'number', 'boolean', 'long', 'date', 'float' +'long', 'integer', 'short', 'byte', 'double', 'float', 'keyword', 'text', 'boolean', 'date' ``` ### Arrays @@ -171,7 +171,7 @@ export const myCollector = makeUsageCollector({ }, some_obj: { total: { - type: 'number', + type: 'long', }, }, some_array: { @@ -182,7 +182,7 @@ export const myCollector = makeUsageCollector({ type: 'array', items: { total: { - type: 'number', + type: 'long', }, }, }, diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 951418d448cbd..73febc0183fc5 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -27,14 +27,9 @@ import { export type CollectorFormatForBulkUpload = (result: T) => { type: string; payload: U }; -export type AllowedSchemaTypes = - | 'keyword' - | 'text' - | 'number' - | 'boolean' - | 'long' - | 'date' - | 'float'; +export type AllowedSchemaNumberTypes = 'long' | 'integer' | 'short' | 'byte' | 'double' | 'float'; + +export type AllowedSchemaTypes = AllowedSchemaNumberTypes | 'keyword' | 'text' | 'boolean' | 'date'; export interface SchemaField { type: string; diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index da85f9ab181c9..2f8be884a8a7b 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -21,6 +21,7 @@ export { CollectorSet } from './collector_set'; export { Collector, AllowedSchemaTypes, + AllowedSchemaNumberTypes, SchemaField, MakeSchemaFrom, CollectorOptions, diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 90483d7c0a4d5..cf3c787e6b0db 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -59,7 +59,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens type: 'boolean', }, authProviderCount: { - type: 'number', + type: 'long', }, enabledAuthProviders: { type: 'array', diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 2b3ff6c8a0aef..c965623ebfc17 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -3250,7 +3250,7 @@ "type": "boolean" }, "authProviderCount": { - "type": "number" + "type": "long" }, "enabledAuthProviders": { "type": "array", From 076bb734c76d8f34b9986b49f1ccaeea8e69de89 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Fri, 30 Oct 2020 06:01:45 +0100 Subject: [PATCH 55/73] Expressions/migrations2 (#81281) --- ...gin-plugins-expressions-public.executor.md | 2 + ...ins-expressions-public.executor.migrate.md | 23 ++++++++++++ ...essions-public.executor.migratetolatest.md | 23 ++++++++++++ ...s-expressions-public.expressionfunction.md | 1 + ...ns-public.expressionfunction.migrations.md | 13 +++++++ ...s-expressions-public.expressionsservice.md | 2 + ...sions-public.expressionsservice.migrate.md | 13 +++++++ ...blic.expressionsservice.migratetolatest.md | 13 +++++++ ...gin-plugins-expressions-server.executor.md | 2 + ...ins-expressions-server.executor.migrate.md | 23 ++++++++++++ ...essions-server.executor.migratetolatest.md | 23 ++++++++++++ ...s-expressions-server.expressionfunction.md | 1 + ...ns-server.expressionfunction.migrations.md | 13 +++++++ .../common/executor/executor.test.ts | 31 ++++++++++++++++ .../expressions/common/executor/executor.ts | 37 ++++++++++++++++++- .../expression_function.ts | 7 +++- .../common/service/expressions_services.ts | 22 ++++++++++- src/plugins/expressions/public/public.api.md | 13 ++++++- src/plugins/expressions/server/server.api.md | 11 +++++- .../common/persistable_state/index.ts | 30 ++++++++++++++- 20 files changed, 296 insertions(+), 7 deletions(-) create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migrate.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md create mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migrate.md create mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md create mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md index aefd04112dc1c..3cc38a0cbdc0f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md @@ -39,6 +39,8 @@ export declare class Executor = Record + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [migrate](./kibana-plugin-plugins-expressions-public.executor.migrate.md) + +## Executor.migrate() method + +Signature: + +```typescript +migrate(ast: SerializableState, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | SerializableState | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md new file mode 100644 index 0000000000000..23b7e6035a0ae --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-public.executor.migratetolatest.md) + +## Executor.migrateToLatest() method + +Signature: + +```typescript +migrateToLatest(ast: unknown, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | unknown | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md index 1815d63d804b1..3e75e9ab3ef6f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md @@ -29,6 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | +| [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | | [name](./kibana-plugin-plugins-expressions-public.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-public.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md new file mode 100644 index 0000000000000..28d521f4b3fe1 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) + +## ExpressionFunction.migrations property + +Signature: + +```typescript +migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md index 041d66b22dd50..307fc73ec6e9c 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md @@ -39,6 +39,8 @@ export declare class ExpressionsService implements PersistableStateExpressionsServiceStart['getType'] | | | [getTypes](./kibana-plugin-plugins-expressions-public.expressionsservice.gettypes.md) | | () => ReturnType<Executor['getTypes']> | Returns POJO map of all registered expression types, where keys are names of the types and values are ExpressionType instances. | | [inject](./kibana-plugin-plugins-expressions-public.expressionsservice.inject.md) | | (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression | Injects saved object references into expression AST | +| [migrate](./kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md) | | (state: SerializableState, version: string) => ExpressionAstExpression | Injects saved object references into expression AST | +| [migrateToLatest](./kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md) | | (state: unknown, version: string) => ExpressionAstExpression | Injects saved object references into expression AST | | [registerFunction](./kibana-plugin-plugins-expressions-public.expressionsservice.registerfunction.md) | | (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void | Register an expression function, which will be possible to execute as part of the expression pipeline.Below we register a function which simply sleeps for given number of milliseconds to delay the execution and outputs its input as-is. ```ts expressions.registerFunction({ diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md new file mode 100644 index 0000000000000..88a6bda4ee3f5 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [migrate](./kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md) + +## ExpressionsService.migrate property + +Injects saved object references into expression AST + +Signature: + +```typescript +readonly migrate: (state: SerializableState, version: string) => ExpressionAstExpression; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md new file mode 100644 index 0000000000000..e6860df19fd3f --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md) + +## ExpressionsService.migrateToLatest property + +Injects saved object references into expression AST + +Signature: + +```typescript +readonly migrateToLatest: (state: unknown, version: string) => ExpressionAstExpression; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md index 97bb3ac895084..da20ae4aa892e 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md @@ -39,6 +39,8 @@ export declare class Executor = Record + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [migrate](./kibana-plugin-plugins-expressions-server.executor.migrate.md) + +## Executor.migrate() method + +Signature: + +```typescript +migrate(ast: SerializableState, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | SerializableState | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md new file mode 100644 index 0000000000000..72e3f8d8b7edc --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-server.executor.migratetolatest.md) + +## Executor.migrateToLatest() method + +Signature: + +```typescript +migrateToLatest(ast: unknown, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | unknown | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md index 7fcda94968d13..00c8aa63bfbd8 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md @@ -29,6 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | +| [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | | [name](./kibana-plugin-plugins-expressions-server.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-server.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md new file mode 100644 index 0000000000000..29031a9306b2f --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) + +## ExpressionFunction.migrations property + +Signature: + +```typescript +migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; +``` diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index a658d3457407c..308d6f7e71814 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -22,6 +22,7 @@ import * as expressionTypes from '../expression_types'; import * as expressionFunctions from '../expression_functions'; import { Execution } from '../execution'; import { ExpressionAstFunction, parseExpression } from '../ast'; +import { MigrateFunction } from '../../../kibana_utils/common/persistable_state'; describe('Executor', () => { test('can instantiate', () => { @@ -158,6 +159,7 @@ describe('Executor', () => { const injectFn = jest.fn().mockImplementation((args, references) => args); const extractFn = jest.fn().mockReturnValue({ args: {}, references: [] }); + const migrateFn = jest.fn().mockImplementation((args) => args); const fooFn = { name: 'foo', @@ -174,6 +176,14 @@ describe('Executor', () => { inject: (state: ExpressionAstFunction['arguments']) => { return injectFn(state); }, + migrations: { + '7.10.0': (((state: ExpressionAstFunction, version: string): ExpressionAstFunction => { + return migrateFn(state, version); + }) as any) as MigrateFunction, + '7.10.1': (((state: ExpressionAstFunction, version: string): ExpressionAstFunction => { + return migrateFn(state, version); + }) as any) as MigrateFunction, + }, fn: jest.fn(), }; executor.registerFunction(fooFn); @@ -194,5 +204,26 @@ describe('Executor', () => { expect(extractFn).toBeCalledTimes(5); }); }); + + describe('.migrate', () => { + test('calls migrate function for every expression function in expression', () => { + executor.migrate( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'), + '7.10.0' + ); + expect(migrateFn).toBeCalledTimes(5); + }); + }); + + describe('.migrateToLatest', () => { + test('calls extract function for every expression function in expression', () => { + migrateFn.mockClear(); + executor.migrateToLatest( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'), + '7.10.0' + ); + expect(migrateFn).toBeCalledTimes(10); + }); + }); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 85b5589b593af..19fc4cf5a14a2 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -32,7 +32,7 @@ import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; import { getByAlias } from '../util'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState } from '../../../kibana_utils/common'; +import { PersistableState, SerializableState } from '../../../kibana_utils/common'; import { ExpressionExecutionParams } from '../service'; export interface ExpressionExecOptions { @@ -88,6 +88,20 @@ export class FunctionsRegistry implements IRegistry { } } +const semverGte = (semver1: string, semver2: string) => { + const regex = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/; + const matches1 = regex.exec(semver1) as RegExpMatchArray; + const matches2 = regex.exec(semver2) as RegExpMatchArray; + + const [, major1, minor1, patch1] = matches1; + const [, major2, minor2, patch2] = matches2; + + return ( + major1 > major2 || + (major1 === major2 && (minor1 > minor2 || (minor1 === minor2 && patch1 >= patch2))) + ); +}; + export class Executor = Record> implements PersistableState { static createWithDefaults = Record>( @@ -249,6 +263,27 @@ export class Executor = Record { + if (!fn.migrations[version]) return link; + const updatedAst = fn.migrations[version](link) as ExpressionAstFunction; + link.arguments = updatedAst.arguments; + link.type = updatedAst.type; + }); + } + + public migrateToLatest(ast: unknown, version: string) { + return this.walkAst(cloneDeep(ast) as ExpressionAstExpression, (fn, link) => { + for (const key of Object.keys(fn.migrations)) { + if (semverGte(key, version)) { + const updatedAst = fn.migrations[key](link) as ExpressionAstFunction; + link.arguments = updatedAst.arguments; + link.type = updatedAst.type; + } + } + }); + } + public fork(): Executor { const initialState = this.state.get(); const fork = new Executor(initialState); diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index 0b56d3c169ff4..2879cc8e3632c 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -24,7 +24,7 @@ import { ExpressionValue } from '../expression_types/types'; import { ExecutionContext } from '../execution'; import { ExpressionAstFunction } from '../ast'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState } from '../../../kibana_utils/common'; +import { PersistableState, SerializableState } from '../../../kibana_utils/common'; export class ExpressionFunction implements PersistableState { /** @@ -76,6 +76,9 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; + migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; constructor(functionDefinition: AnyExpressionFunctionDefinition) { const { @@ -91,6 +94,7 @@ export class ExpressionFunction implements PersistableState c); this.inject = inject || identity; this.extract = extract || ((s) => ({ state: s, references: [] })); + this.migrations = migrations || {}; for (const [key, arg] of Object.entries(args || {})) { this.args[key] = new ExpressionFunctionParameter(key, arg); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index abbba433ab3ca..0f898563c3d0e 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -24,7 +24,7 @@ import { ExecutionContract } from '../execution/execution_contract'; import { AnyExpressionTypeDefinition } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState } from '../../../kibana_utils/common'; +import { PersistableState, SerializableState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; import { ExecutionContextSearch } from '../execution'; @@ -303,6 +303,26 @@ export class ExpressionsService implements PersistableState { + return this.executor.migrate(state, version); + }; + + /** + * Migrates expression to the latest version + * @param state expression AST to update + * @param version the version of kibana in which expression was created + * @returns migrated expression AST + */ + public readonly migrateToLatest = (state: unknown, version: string) => { + return this.executor.migrateToLatest(state, version); + }; + /** * Returns Kibana Platform *setup* life-cycle contract. Useful to return the * same contract on server-side and browser-side. diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 68a3507bbf166..5ca085ebb0d1d 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -222,6 +222,12 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -342,6 +348,10 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; + // (undocumented) + migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; name: string; // (undocumented) telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; @@ -586,6 +596,8 @@ export class ExpressionsService implements PersistableState ReturnType; readonly inject: (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression; + readonly migrate: (state: SerializableState, version: string) => ExpressionAstExpression; + readonly migrateToLatest: (state: unknown, version: string) => ExpressionAstExpression; readonly registerFunction: (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void; // (undocumented) readonly registerRenderer: (definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) => void; @@ -1149,7 +1161,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // // src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index d6925a027358c..c8d5464929033 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -204,6 +204,12 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -314,6 +320,10 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; + // (undocumented) + migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; name: string; // (undocumented) telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; @@ -939,7 +949,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // // src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/kibana_utils/common/persistable_state/index.ts b/src/plugins/kibana_utils/common/persistable_state/index.ts index ae5e3d514554c..40feea3f24f28 100644 --- a/src/plugins/kibana_utils/common/persistable_state/index.ts +++ b/src/plugins/kibana_utils/common/persistable_state/index.ts @@ -27,6 +27,11 @@ export type SerializableState = { [key: string]: Serializable; }; +export type MigrateFunction< + FromVersion extends SerializableState = SerializableState, + ToVersion extends SerializableState = SerializableState +> = (state: FromVersion) => ToVersion; + export interface PersistableState

{ /** * function to extract telemetry information @@ -47,8 +52,29 @@ export interface PersistableState

{ state: P; references: SavedObjectReference[] }; + + /** + * migrateToLatest function receives state of older version and should migrate to the latest version + * @param state + * @param version + */ + migrateToLatest?: (state: SerializableState, version: string) => P; + + /** + * migrate function runs the specified migration + * @param state + * @param version + */ + migrate?: (state: SerializableState, version: string) => SerializableState; } export type PersistableStateDefinition

= Partial< - PersistableState

->; + Omit, 'migrate'> +> & { + /** + * list of all migrations per semver + */ + migrations?: { + [key: string]: MigrateFunction; + }; +}; From 534f4e85386b0e208f460c354730bd9367f025e4 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2020 16:52:30 +0900 Subject: [PATCH 56/73] [TSVB] Renamed 'positive rate' to 'counter rate' (#80939) * Renamed 'positive rate' label text to 'counter rate' * Removed translations(ja-JP, zh-CN) where the labels were updated Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_type_timeseries/common/agg_lookup.js | 2 +- src/plugins/vis_type_timeseries/common/calculate_label.js | 2 +- .../public/application/components/aggs/agg_select.test.tsx | 4 ++-- .../public/application/components/aggs/agg_select.tsx | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 3 --- x-pack/plugins/translations/translations/zh-CN.json | 3 --- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.js b/src/plugins/vis_type_timeseries/common/agg_lookup.js index 432da03e3d45d..0a71ab34082f8 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.js @@ -98,7 +98,7 @@ export const lookup = { }), top_hit: i18n.translate('visTypeTimeseries.aggLookup.topHitLabel', { defaultMessage: 'Top Hit' }), positive_rate: i18n.translate('visTypeTimeseries.aggLookup.positiveRateLabel', { - defaultMessage: 'Positive Rate', + defaultMessage: 'Counter Rate', }), }; diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.js b/src/plugins/vis_type_timeseries/common/calculate_label.js index 9f3030eeb6eae..96e9fa0825b25 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.js @@ -72,7 +72,7 @@ export function calculateLabel(metric, metrics) { } if (metric.type === 'positive_rate') { return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', { - defaultMessage: 'Positive Rate of {field}', + defaultMessage: 'Counter Rate of {field}', values: { field: metric.field }, }); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx index 968fa5384e1d8..6c75e081429de 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx @@ -63,7 +63,7 @@ describe('TSVB AggSelect', () => { "value": "count", }, Object { - "label": "Positive Rate", + "label": "Counter Rate", "value": "positive_rate", }, Object { @@ -131,7 +131,7 @@ describe('TSVB AggSelect', () => { "value": "filter_ratio", }, Object { - "label": "Positive Rate", + "label": "Counter Rate", "value": "positive_rate", }, Object { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx index 7701d351e5478..5c8049e363694 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx @@ -54,7 +54,7 @@ const metricAggs: AggSelectOption[] = [ }, { label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.positiveRateLabel', { - defaultMessage: 'Positive Rate', + defaultMessage: 'Counter Rate', }), value: 'positive_rate', }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1492c8a03906a..cd090b0c264ef 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3801,7 +3801,6 @@ "visTypeTimeseries.aggLookup.percentileLabel": "パーセンタイル", "visTypeTimeseries.aggLookup.percentileRankLabel": "パーセンタイルランク", "visTypeTimeseries.aggLookup.positiveOnlyLabel": "プラスのみ", - "visTypeTimeseries.aggLookup.positiveRateLabel": "正の割合", "visTypeTimeseries.aggLookup.serialDifferenceLabel": "連続差", "visTypeTimeseries.aggLookup.seriesAggLabel": "数列集約", "visTypeTimeseries.aggLookup.staticValueLabel": "不動値", @@ -3824,7 +3823,6 @@ "visTypeTimeseries.aggSelect.metricsAggs.minLabel": "最低", "visTypeTimeseries.aggSelect.metricsAggs.percentileLabel": "パーセンタイル", "visTypeTimeseries.aggSelect.metricsAggs.percentileRankLabel": "パーセンタイルランク", - "visTypeTimeseries.aggSelect.metricsAggs.positiveRateLabel": "正の割合", "visTypeTimeseries.aggSelect.metricsAggs.staticValueLabel": "不動値", "visTypeTimeseries.aggSelect.metricsAggs.stdDeviationLabel": "標準偏差", "visTypeTimeseries.aggSelect.metricsAggs.sumLabel": "合計", @@ -3868,7 +3866,6 @@ "visTypeTimeseries.calculateLabel.lookupMetricTypeOfTargetLabel": "{targetLabel} 中 {lookupMetricType}", "visTypeTimeseries.calculateLabel.lookupMetricTypeOfTargetWithAdditionalLabel": "{targetLabel} ({additionalLabel}) 中 {lookupMetricType}", "visTypeTimeseries.calculateLabel.mathLabel": "数学処理", - "visTypeTimeseries.calculateLabel.positiveRateLabel": "{field}の正の割合", "visTypeTimeseries.calculateLabel.seriesAggLabel": "数列集約 ({metricFunction})", "visTypeTimeseries.calculateLabel.staticValueLabel": "{metricValue} の不動値", "visTypeTimeseries.calculateLabel.unknownLabel": "不明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index be5bcd3cf0543..18403d85d5ff7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3802,7 +3802,6 @@ "visTypeTimeseries.aggLookup.percentileLabel": "百分位数", "visTypeTimeseries.aggLookup.percentileRankLabel": "百分位数排名", "visTypeTimeseries.aggLookup.positiveOnlyLabel": "仅正数", - "visTypeTimeseries.aggLookup.positiveRateLabel": "正比率", "visTypeTimeseries.aggLookup.serialDifferenceLabel": "串行差分", "visTypeTimeseries.aggLookup.seriesAggLabel": "序列聚合", "visTypeTimeseries.aggLookup.staticValueLabel": "静态值", @@ -3825,7 +3824,6 @@ "visTypeTimeseries.aggSelect.metricsAggs.minLabel": "最小值", "visTypeTimeseries.aggSelect.metricsAggs.percentileLabel": "百分位数", "visTypeTimeseries.aggSelect.metricsAggs.percentileRankLabel": "百分位数排名", - "visTypeTimeseries.aggSelect.metricsAggs.positiveRateLabel": "正比率", "visTypeTimeseries.aggSelect.metricsAggs.staticValueLabel": "静态值", "visTypeTimeseries.aggSelect.metricsAggs.stdDeviationLabel": "标准偏差", "visTypeTimeseries.aggSelect.metricsAggs.sumLabel": "和", @@ -3869,7 +3867,6 @@ "visTypeTimeseries.calculateLabel.lookupMetricTypeOfTargetLabel": "{targetLabel} 的 {lookupMetricType}", "visTypeTimeseries.calculateLabel.lookupMetricTypeOfTargetWithAdditionalLabel": "{targetLabel} ({additionalLabel}) 的 {lookupMetricType}", "visTypeTimeseries.calculateLabel.mathLabel": "数学", - "visTypeTimeseries.calculateLabel.positiveRateLabel": "{field} 的正比率", "visTypeTimeseries.calculateLabel.seriesAggLabel": "序列聚合 ({metricFunction})", "visTypeTimeseries.calculateLabel.staticValueLabel": "{metricValue} 的静态值", "visTypeTimeseries.calculateLabel.unknownLabel": "未知", From e01fc2f09bff7eb3e7fba255ed16e69355c50673 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Fri, 30 Oct 2020 03:24:03 -0600 Subject: [PATCH 57/73] Remove legacy app arch items from codeowners. (#82084) --- .github/CODEOWNERS | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 28380549c751c..36e9f4220b35c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -39,7 +39,10 @@ #CC# /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app #CC# /src/legacy/server/url_shortening/ @elastic/kibana-app #CC# /src/legacy/ui/public/state_management @elastic/kibana-app +#CC# /src/plugins/advanced_settings/ @elastic/kibana-app +#CC# /src/plugins/charts/ @elastic/kibana-app #CC# /src/plugins/index_pattern_management/public @elastic/kibana-app +#CC# /src/plugins/vis_default_editor @elastic/kibana-app # App Architecture /examples/bfetch_explorer/ @elastic/kibana-app-arch @@ -70,23 +73,10 @@ /x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/status_page/public @elastic/kibana-app-arch -#CC# /src/legacy/server/index_patterns/ @elastic/kibana-app-arch -#CC# /src/legacy/ui/public/field_editor @elastic/kibana-app-arch -#CC# /src/legacy/ui/public/management @elastic/kibana-app-arch -#CC# /src/plugins/advanced_settings/ @elastic/kibana-app-arch #CC# /src/plugins/bfetch/ @elastic/kibana-app-arch -#CC# /src/plugins/charts/ @elastic/kibana-app-arch #CC# /src/plugins/index_pattern_management/public/service @elastic/kibana-app-arch #CC# /src/plugins/inspector/ @elastic/kibana-app-arch -#CC# /src/plugins/saved_objects/ @elastic/kibana-app-arch #CC# /src/plugins/share/ @elastic/kibana-app-arch -#CC# /src/plugins/vis_default_editor @elastic/kibana-app-arch #CC# /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch #CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch #CC# /packages/kbn-interpreter/ @elastic/kibana-app-arch @@ -243,6 +233,7 @@ #CC# /src/legacy/ui/public/documentation_links @elastic/kibana-platform #CC# /src/legacy/ui/public/autoload @elastic/kibana-platform #CC# /src/plugins/legacy_export/ @elastic/kibana-platform +#CC# /src/plugins/saved_objects/ @elastic/kibana-platform #CC# /src/plugins/status_page/ @elastic/kibana-platform #CC# /src/plugins/testbed/server/ @elastic/kibana-platform #CC# /x-pack/legacy/plugins/xpack_main/server/ @elastic/kibana-platform From b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 30 Oct 2020 10:40:23 +0100 Subject: [PATCH 58/73] [ML] Data Frame Analytics: Fix feature importance cell value and decision path chart (#82011) Fixes a regression that caused data grid cells for feature importance to be empty and clicking on the button to show the decision path chart popover to render the whole page empty. --- .../ml/common/types/feature_importance.ts | 5 +- .../components/data_grid/common.test.ts | 2 +- .../components/data_grid/common.ts | 95 +++++++++++++++++++ .../components/data_grid/data_grid.tsx | 27 ++++-- .../data_frame_analytics/common/fields.ts | 8 ++ .../common/get_index_data.ts | 2 +- .../exploration_results_table.tsx | 2 +- 7 files changed, 131 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts index 4f5619cf3ab7b..1ae4c7832390c 100644 --- a/x-pack/plugins/ml/common/types/feature_importance.ts +++ b/x-pack/plugins/ml/common/types/feature_importance.ts @@ -8,10 +8,13 @@ export interface ClassFeatureImportance { class_name: string | boolean; importance: number; } + +// TODO We should separate the interface because classes/importance +// isn't both optional but either/or. export interface FeatureImportance { feature_name: string; - importance?: number; classes?: ClassFeatureImportance[]; + importance?: number; } export interface TopClass { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts index 4bb670ad02dfc..aaf6f90b00f4d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts @@ -8,7 +8,7 @@ import { EuiDataGridSorting } from '@elastic/eui'; import { multiColumnSortFactory } from './common'; -describe('Transform: Define Pivot Common', () => { +describe('Data Frame Analytics: Data Grid Common', () => { test('multiColumnSortFactory()', () => { const data = [ { s: 'a', n: 1 }, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 642d0ae564b85..48a0a0c9ab126 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -24,7 +24,9 @@ import { KBN_FIELD_TYPES, } from '../../../../../../../src/plugins/data/public'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { extractErrorMessage } from '../../../../common/util/errors'; +import { FeatureImportance, TopClasses } from '../../../../common/types/feature_importance'; import { BASIC_NUMERICAL_TYPES, @@ -158,6 +160,90 @@ export const getDataGridSchemaFromKibanaFieldType = ( return schema; }; +const getClassName = (className: string, isClassTypeBoolean: boolean) => { + if (isClassTypeBoolean) { + return className === 'true'; + } + + return className; +}; +/** + * Helper to transform feature importance flattened fields with arrays back to object structure + * + * @param row - EUI data grid data row + * @param mlResultsField - Data frame analytics results field + * @returns nested object structure of feature importance values + */ +export const getFeatureImportance = ( + row: Record, + mlResultsField: string, + isClassTypeBoolean = false +): FeatureImportance[] => { + const featureNames: string[] | undefined = + row[`${mlResultsField}.feature_importance.feature_name`]; + const classNames: string[] | undefined = + row[`${mlResultsField}.feature_importance.classes.class_name`]; + const classImportance: number[] | undefined = + row[`${mlResultsField}.feature_importance.classes.importance`]; + + if (featureNames === undefined) { + return []; + } + + // return object structure for classification job + if (classNames !== undefined && classImportance !== undefined) { + const overallClassNames = classNames?.slice(0, classNames.length / featureNames.length); + + return featureNames.map((fName, index) => { + const offset = overallClassNames.length * index; + const featureClassImportance = classImportance.slice( + offset, + offset + overallClassNames.length + ); + return { + feature_name: fName, + classes: overallClassNames.map((fClassName, fIndex) => { + return { + class_name: getClassName(fClassName, isClassTypeBoolean), + importance: featureClassImportance[fIndex], + }; + }), + }; + }); + } + + // return object structure for regression job + const importance: number[] = row[`${mlResultsField}.feature_importance.importance`]; + return featureNames.map((fName, index) => ({ + feature_name: fName, + importance: importance[index], + })); +}; + +/** + * Helper to transforms top classes flattened fields with arrays back to object structure + * + * @param row - EUI data grid data row + * @param mlResultsField - Data frame analytics results field + * @returns nested object structure of feature importance values + */ +export const getTopClasses = (row: Record, mlResultsField: string): TopClasses => { + const classNames: string[] | undefined = row[`${mlResultsField}.top_classes.class_name`]; + const classProbabilities: number[] | undefined = + row[`${mlResultsField}.top_classes.class_probability`]; + const classScores: number[] | undefined = row[`${mlResultsField}.top_classes.class_score`]; + + if (classNames === undefined || classProbabilities === undefined || classScores === undefined) { + return []; + } + + return classNames.map((className, index) => ({ + class_name: className, + class_probability: classProbabilities[index], + class_score: classScores[index], + })); +}; + export const useRenderCellValue = ( indexPattern: IndexPattern | undefined, pagination: IndexPagination, @@ -207,6 +293,15 @@ export const useRenderCellValue = ( return item[cId]; } + // For classification and regression results, we need to treat some fields with a custom transform. + if (cId === `${resultsField}.feature_importance`) { + return getFeatureImportance(fullItem, resultsField ?? DEFAULT_RESULTS_FIELD); + } + + if (cId === `${resultsField}.top_classes`) { + return getTopClasses(fullItem, resultsField ?? DEFAULT_RESULTS_FIELD); + } + // Try if the field name is available as a nested field. return getNestedProperty(tableItems[adjustedRowIndex], cId, null); } diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index fad2439f5d5ee..50e9cabc99c35 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -27,10 +27,15 @@ import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../common/constants/field_h import { ANALYSIS_CONFIG_TYPE, INDEX_STATUS } from '../../data_frame_analytics/common'; -import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; +import { + euiDataGridStyle, + euiDataGridToolbarSettings, + getFeatureImportance, + getTopClasses, +} from './common'; import { UseIndexDataReturnType } from './types'; import { DecisionPathPopover } from './feature_importance/decision_path_popover'; -import { TopClasses } from '../../../../common/types/feature_importance'; +import { FeatureImportance, TopClasses } from '../../../../common/types/feature_importance'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics'; @@ -118,18 +123,28 @@ export const DataGrid: FC = memo( if (!row) return

; // if resultsField for some reason is not available then use ml const mlResultsField = resultsField ?? DEFAULT_RESULTS_FIELD; - const parsedFIArray = row[mlResultsField].feature_importance; let predictedValue: string | number | undefined; let topClasses: TopClasses = []; if ( predictionFieldName !== undefined && row && - row[mlResultsField][predictionFieldName] !== undefined + row[`${mlResultsField}.${predictionFieldName}`] !== undefined ) { - predictedValue = row[mlResultsField][predictionFieldName]; - topClasses = row[mlResultsField].top_classes; + predictedValue = row[`${mlResultsField}.${predictionFieldName}`]; + topClasses = getTopClasses(row, mlResultsField); } + const isClassTypeBoolean = topClasses.reduce( + (p, c) => typeof c.class_name === 'boolean' || p, + false + ); + + const parsedFIArray: FeatureImportance[] = getFeatureImportance( + row, + mlResultsField, + isClassTypeBoolean + ); + return ( !field.name.includes(`${resultsField}.${FEATURE_IMPORTANCE}.`) + ); } if ((numTopClasses ?? 0) > 0) { @@ -221,6 +225,10 @@ export const getDefaultFieldsFromJobCaps = ( name: `${resultsField}.${TOP_CLASSES}`, type: KBN_FIELD_TYPES.UNKNOWN, }); + // remove flattened top classes fields + fields = fields.filter( + (field: any) => !field.name.includes(`${resultsField}.${TOP_CLASSES}.`) + ); } // Only need to add these fields if we didn't use dest index pattern to get the fields diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index 8e50aab0914db..85f222109d408 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -53,7 +53,7 @@ export const getIndexData = async ( index: jobConfig.dest.index, body: { fields: ['*'], - _source: [], + _source: false, query: searchQuery, from: pageIndex * pageSize, size: pageSize, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index a6e95269b3633..10e2ad5b5eb53 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -29,7 +29,7 @@ interface Props { } export const ExplorationResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, needsDestIndexPattern, searchQuery }) => { + ({ indexPattern, jobConfig, needsDestIndexPattern, searchQuery }) => { const { services: { mlServices: { mlApiServices }, From 512c35e70b88f28d0df914873f9ff4ff96ccf6a4 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 30 Oct 2020 11:20:54 +0100 Subject: [PATCH 59/73] fix Lens heading structure (#81752) --- .../workspace_panel/workspace_panel_wrapper.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index fa63cd3c6f1e0..5cfc269dbb97b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -15,6 +15,7 @@ import { EuiPageContentHeader, EuiFlexGroup, EuiFlexItem, + EuiScreenReaderOnly, } from '@elastic/eui'; import { Datasource, FramePublicAPI, Visualization } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; @@ -104,18 +105,25 @@ export function WorkspacePanelWrapper({
- {(!emptyExpression || title) && ( + {!emptyExpression || title ? ( - +

{title || i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} - +

+ ) : ( + +

+ {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} +

+
)} {children} From aa620bcbb0a26a3a5a5d18b8c81226f7a9f4ad61 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 30 Oct 2020 11:43:34 +0100 Subject: [PATCH 60/73] [Search] Add "restore" to session service (#81924) --- ...plugin-plugins-data-public.isearchsetup.md | 2 +- ...lugins-data-public.isearchsetup.session.md | 2 +- ...plugin-plugins-data-public.isearchstart.md | 2 +- ...lugins-data-public.isearchstart.session.md | 2 +- ...ugins-data-public.isessionservice.clear.md | 13 +++++++++++ ...data-public.isessionservice.getsession_.md | 13 +++++++++++ ...ata-public.isessionservice.getsessionid.md | 13 +++++++++++ ...gin-plugins-data-public.isessionservice.md | 22 +++++++++++++++++++ ...ins-data-public.isessionservice.restore.md | 13 +++++++++++ ...ugins-data-public.isessionservice.start.md | 13 +++++++++++ .../kibana-plugin-plugins-data-public.md | 1 + .../data/common/search/session/mocks.ts | 1 + .../data/common/search/session/types.ts | 6 +++++ src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/public.api.md | 14 +++++++++--- src/plugins/data/public/search/index.ts | 1 + .../public/search/session_service.test.ts | 16 ++++++++++++++ .../data/public/search/session_service.ts | 22 +++++++++++-------- 18 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index bbf856480aedd..b2f8e83d8e654 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,6 +17,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | -| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md index 7f39d9714a3a3..739fdfdeb5fc3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -4,7 +4,7 @@ ## ISearchSetup.session property -session management +session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index 4a69e94dd6f58..dba60c7bdf147 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,6 +19,6 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | -| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md index de25cccd6d27a..1ad194a9bec86 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -4,7 +4,7 @@ ## ISearchStart.session property -session management +session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md new file mode 100644 index 0000000000000..fc3d214eb4cad --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) + +## ISessionService.clear property + +Clears the active session. + +Signature: + +```typescript +clear: () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md new file mode 100644 index 0000000000000..e30c89fb1a9fd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) + +## ISessionService.getSession$ property + +Returns the observable that emits an update every time the session ID changes + +Signature: + +```typescript +getSession$: () => Observable; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md new file mode 100644 index 0000000000000..838023ff1d8b9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) + +## ISessionService.getSessionId property + +Returns the active session ID + +Signature: + +```typescript +getSessionId: () => string | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md new file mode 100644 index 0000000000000..174f9dbe66bf4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) + +## ISessionService interface + +Signature: + +```typescript +export interface ISessionService +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void | Clears the active session. | +| [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined> | Returns the observable that emits an update every time the session ID changes | +| [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined | Returns the active session ID | +| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => void | Restores existing session | +| [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string | Starts a new session | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md new file mode 100644 index 0000000000000..857e85bbd30eb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) + +## ISessionService.restore property + +Restores existing session + +Signature: + +```typescript +restore: (sessionId: string) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md new file mode 100644 index 0000000000000..9e14c5ed26765 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) + +## ISessionService.start property + +Starts a new session + +Signature: + +```typescript +start: () => string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 6a3c437305cc8..ac6923fd12f96 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -74,6 +74,7 @@ | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | | [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service | | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | +| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts index 7d5cd75b57534..2b64bbbd27565 100644 --- a/src/plugins/data/common/search/session/mocks.ts +++ b/src/plugins/data/common/search/session/mocks.ts @@ -23,6 +23,7 @@ export function getSessionServiceMock(): jest.Mocked { return { clear: jest.fn(), start: jest.fn(), + restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(), }; diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index 80ab74f1aa14d..6660b8395547f 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -34,6 +34,12 @@ export interface ISessionService { * Starts a new session */ start: () => string; + + /** + * Restores existing session + */ + restore: (sessionId: string) => void; + /** * Clears the active session. */ diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index c041511745be2..c54cb36142cbd 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -380,7 +380,7 @@ export { PainlessError, } from './search'; -export type { SearchSource } from './search'; +export type { SearchSource, ISessionService } from './search'; export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 7ee21236c1c79..b072407a5fe10 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1415,8 +1415,6 @@ export interface ISearchSetup { // // (undocumented) aggs: AggsSetup; - // Warning: (ae-forgotten-export) The symbol "ISessionService" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ISessionService" session: ISessionService; // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts // @@ -1432,7 +1430,6 @@ export interface ISearchStart { aggs: AggsStart; search: ISearchGeneric; searchSource: ISearchStartSearchSource; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ISessionService" session: ISessionService; // (undocumented) showError: (e: Error) => void; @@ -1449,6 +1446,17 @@ export interface ISearchStartSearchSource { // @public (undocumented) export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined; +// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ISessionService { + clear: () => void; + getSession$: () => Observable; + getSessionId: () => string | undefined; + restore: (sessionId: string) => void; + start: () => string; +} + // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 86804a819cb0e..1abf3192a4846 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -41,6 +41,7 @@ export { SearchSourceDependencies, SearchSourceFields, SortDirection, + ISessionService, } from '../../common/search'; export { getEsPreference } from './es_search'; diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session_service.test.ts index dd64d187f47d6..bcfd06944d983 100644 --- a/src/plugins/data/public/search/session_service.test.ts +++ b/src/plugins/data/public/search/session_service.test.ts @@ -20,6 +20,7 @@ import { SessionService } from './session_service'; import { ISessionService } from '../../common'; import { coreMock } from '../../../../core/public/mocks'; +import { take, toArray } from 'rxjs/operators'; describe('Session service', () => { let sessionService: ISessionService; @@ -39,5 +40,20 @@ describe('Session service', () => { sessionService.clear(); expect(sessionService.getSessionId()).toBeUndefined(); }); + + it('Restores a session', async () => { + const sessionId = 'sessionId'; + sessionService.restore(sessionId); + expect(sessionService.getSessionId()).toBe(sessionId); + }); + + it('sessionId$ observable emits current value', async () => { + sessionService.restore('1'); + const emittedValues = sessionService.getSession$().pipe(take(3), toArray()).toPromise(); + sessionService.restore('2'); + sessionService.clear(); + + expect(await emittedValues).toEqual(['1', '2', undefined]); + }); }); }); diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts index 31524434af302..a172738812937 100644 --- a/src/plugins/data/public/search/session_service.ts +++ b/src/plugins/data/public/search/session_service.ts @@ -18,14 +18,16 @@ */ import uuid from 'uuid'; -import { Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subscription } from 'rxjs'; import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; -import { ISessionService } from '../../common/search'; import { ConfigSchema } from '../../config'; +import { ISessionService } from '../../common/search'; export class SessionService implements ISessionService { - private sessionId?: string; - private session$: Subject = new Subject(); + private session$ = new BehaviorSubject(undefined); + private get sessionId() { + return this.session$.getValue(); + } private appChangeSubscription$?: Subscription; private curApp?: string; @@ -68,13 +70,15 @@ export class SessionService implements ISessionService { } public start() { - this.sessionId = uuid.v4(); - this.session$.next(this.sessionId); - return this.sessionId; + this.session$.next(uuid.v4()); + return this.sessionId!; + } + + public restore(sessionId: string) { + this.session$.next(sessionId); } public clear() { - this.sessionId = undefined; - this.session$.next(this.sessionId); + this.session$.next(undefined); } } From 2aeeb6e5c5959c345f372d2b40db4ceb5e5f4ebc Mon Sep 17 00:00:00 2001 From: Bo Andersen Date: Fri, 30 Oct 2020 12:07:10 +0100 Subject: [PATCH 61/73] Fixed dead links (#78696) Fixed links to 404 page. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/monitoring/beats-details.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index 3d7a726d2f8a2..dbd9aecd04768 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -9,7 +9,7 @@ If you are monitoring Beats, the *Stack Monitoring* page in {kib} contains a panel for Beats in the cluster overview. [role="screenshot"] -image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="images/monitoring-beats.jpg"] +image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="user/monitoring/images/monitoring-beats.jpg"] To view an overview of the Beats data in the cluster, click *Overview*. The overview page has a section for activity in the last day, which is a real-time @@ -24,7 +24,7 @@ cluster. All columns are sortable. Clicking a Beat name takes you to the detail page. For example: [role="screenshot"] -image::user/monitoring/images/monitoring-beats-detail.jpg["Monitoring details for Filebeat",link="images/monitoring-beats-detail.jpg"] +image::user/monitoring/images/monitoring-beats-detail.jpg["Monitoring details for Filebeat",link="user/monitoring/images/monitoring-beats-detail.jpg"] The detail page contains a summary bar and charts. There are more charts on this page than the overview page and they are specific to a single Beat instance. From 21615c16ef40e2d740bb7ff64933e69307785b05 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 30 Oct 2020 12:59:28 +0100 Subject: [PATCH 62/73] SO management: fix legacy import index pattern selection being reset when switching page (#81621) * fix legacy import index pattern selection being reset when switching pages * update snapshots --- .../__snapshots__/flyout.test.tsx.snap | 10 ++ .../objects_table/components/flyout.tsx | 50 ++++++-- .../apps/management/_import_objects.ts | 43 +++++-- ...rt_objects_missing_all_index_patterns.json | 121 ++++++++++++++++++ .../management/saved_objects_page.ts | 6 + 5 files changed, 204 insertions(+), 26 deletions(-) create mode 100644 test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 3a03c5c01b3c2..ea86ea58faf61 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -101,8 +101,11 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` }, ] } + onTableChange={[Function]} pagination={ Object { + "pageIndex": 0, + "pageSize": 5, "pageSizeOptions": Array [ 5, 10, @@ -246,6 +249,10 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "newIndexPatternId": "2", }, ], + "unmatchedReferencesTablePagination": Object { + "pageIndex": 0, + "pageSize": 5, + }, }, }, ], @@ -403,8 +410,11 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` }, ] } + onTableChange={[Function]} pagination={ Object { + "pageIndex": 0, + "pageSize": 5, "pageSizeOptions": Array [ 5, 10, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 3165ea4ca0794..75792becc29d8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -88,6 +88,7 @@ export interface FlyoutState { conflictedSavedObjectsLinkedToSavedSearches?: any[]; conflictedSearchDocs?: any[]; unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; + unmatchedReferencesTablePagination: { pageIndex: number; pageSize: number }; failedImports?: ProcessedImportResponse['failedImports']; successfulImports?: ProcessedImportResponse['successfulImports']; conflictingRecord?: ConflictingRecord; @@ -115,6 +116,10 @@ export class Flyout extends Component { conflictedSavedObjectsLinkedToSavedSearches: undefined, conflictedSearchDocs: undefined, unmatchedReferences: undefined, + unmatchedReferencesTablePagination: { + pageIndex: 0, + pageSize: 5, + }, conflictingRecord: undefined, error: undefined, file: undefined, @@ -467,7 +472,7 @@ export class Flyout extends Component { }; renderUnmatchedReferences() { - const { unmatchedReferences } = this.state; + const { unmatchedReferences, unmatchedReferencesTablePagination: tablePagination } = this.state; if (!unmatchedReferences) { return null; @@ -527,22 +532,28 @@ export class Flyout extends Component { { defaultMessage: 'New index pattern' } ), render: (id: string) => { - const options = this.state.indexPatterns!.map( - (indexPattern) => - ({ - text: indexPattern.title, - value: indexPattern.id, - 'data-test-subj': `indexPatternOption-${indexPattern.title}`, - } as { text: string; value: string; 'data-test-subj'?: string }) - ); - - options.unshift({ - text: '-- Skip Import --', - value: '', - }); + const options = [ + { + text: '-- Skip Import --', + value: '', + }, + ...this.state.indexPatterns!.map( + (indexPattern) => + ({ + text: indexPattern.title, + value: indexPattern.id, + 'data-test-subj': `indexPatternOption-${indexPattern.title}`, + } as { text: string; value: string; 'data-test-subj'?: string }) + ), + ]; + + const selectedValue = + unmatchedReferences?.find((unmatchedRef) => unmatchedRef.existingIndexPatternId === id) + ?.newIndexPatternId ?? ''; return ( this.onIndexChanged(id, e)} options={options} @@ -553,6 +564,7 @@ export class Flyout extends Component { ]; const pagination = { + ...tablePagination, pageSizeOptions: [5, 10, 25], }; @@ -561,6 +573,16 @@ export class Flyout extends Component { items={unmatchedReferences as any[]} columns={columns} pagination={pagination} + onTableChange={({ page }) => { + if (page) { + this.setState({ + unmatchedReferencesTablePagination: { + pageSize: page.size, + pageIndex: page.index, + }, + }); + } + }} /> ); } diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 0b417d7d23e93..ddabb32bf5909 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -421,21 +421,40 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(isSavedObjectImported).to.be(true); }); - it('should import saved objects with index patterns when index patterns does not exists', async () => { - // First, we need to delete the index pattern - await PageObjects.savedObjects.clickCheckboxByTitle('logstash-*'); - await PageObjects.savedObjects.clickDelete(); - - // Then, import the objects + it('should preserve index patterns selection when switching between pages', async () => { await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') + path.join(__dirname, 'exports', '_import_objects_missing_all_index_patterns.json') ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object imported with index pattern'); - expect(isSavedObjectImported).to.be(true); + await PageObjects.savedObjects.setOverriddenIndexPatternValue( + 'missing-index-pattern-1', + 'index-pattern-test-1' + ); + + await testSubjects.click('pagination-button-next'); + + await PageObjects.savedObjects.setOverriddenIndexPatternValue( + 'missing-index-pattern-7', + 'index-pattern-test-2' + ); + + await testSubjects.click('pagination-button-previous'); + + const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute( + 'managementChangeIndexSelection-missing-index-pattern-1', + 'value' + ); + + expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20'); + + await testSubjects.click('pagination-button-next'); + + const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute( + 'managementChangeIndexSelection-missing-index-pattern-7', + 'value' + ); + + expect(selectedIdForMissingIndexPattern7).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a87'); }); }); }); diff --git a/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json b/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json new file mode 100644 index 0000000000000..45572b0bf34fe --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json @@ -0,0 +1,121 @@ +[ + { + "_id": "test-vis-1", + "_type": "visualization", + "_source": { + "title": "Test VIS 1", + "visState": "{\"title\":\"test vis 1\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-1\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-2", + "_type": "visualization", + "_source": { + "title": "Test VIS 2", + "visState": "{\"title\":\"test vis 2\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-2\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-3", + "_type": "visualization", + "_source": { + "title": "Test VIS 3", + "visState": "{\"title\":\"test vis 3\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-3\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-4", + "_type": "visualization", + "_source": { + "title": "Test VIS 4", + "visState": "{\"title\":\"test vis 4\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-4\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-5", + "_type": "visualization", + "_source": { + "title": "Test VIS 5", + "visState": "{\"title\":\"test vis 5\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-5\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-6", + "_type": "visualization", + "_source": { + "title": "Test VIS 6", + "visState": "{\"title\":\"test vis 6\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-6\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-7", + "_type": "visualization", + "_source": { + "title": "Test VIS 7", + "visState": "{\"title\":\"test vis 7\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-7\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + } +] diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 8e65b6488836c..0742f14b2eeb4 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -126,6 +126,12 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv } } + async setOverriddenIndexPatternValue(oldName: string, newName: string) { + const select = await testSubjects.find(`managementChangeIndexSelection-${oldName}`); + const option = await testSubjects.findDescendant(`indexPatternOption-${newName}`, select); + await option.click(); + } + async clickCopyToSpaceByTitle(title: string) { const table = keyBy(await this.getElementsInTable(), 'title'); // should we check if table size > 0 and log error if not? From aaadbe88c5b6e5d08e2ccb1bdda753f84858d94d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 30 Oct 2020 13:04:48 +0100 Subject: [PATCH 63/73] Context menu trigger for URL Drilldown (#81158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add context menu trigger to URL drilldown * fix: 🐛 translate "Drilldowns" grouping title * feat: 🎸 add dynamic action grouping to dynamic actions * fix: 🐛 add translations to trigger texts * feat: 🎸 enambe ctx menu trigger in both flyouts, move to end * fix: 🐛 show context menu event scope variable sfor ctx menu * test: 💍 add tests * fix: 🐛 use correct namespace for translation keys * docs: ✏️ update autogenerated docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...able-public.iscontextmenutriggercontext.md | 11 +++++ ...kibana-plugin-plugins-embeddable-public.md | 1 + src/plugins/embeddable/public/index.ts | 1 + .../public/lib/triggers/triggers.ts | 47 +++++++++++++------ src/plugins/embeddable/public/public.api.md | 7 ++- .../drilldowns/actions/drilldown_shared.ts | 5 +- .../flyout_create_drilldown.test.tsx | 2 +- .../flyout_create_drilldown.tsx | 11 +++-- .../flyout_edit_drilldown.tsx | 12 +++-- .../public/lib/url_drilldown.tsx | 13 +++-- .../public/lib/url_drilldown_scope.test.ts | 47 +++++++++++-------- .../public/lib/url_drilldown_scope.ts | 21 +++++++-- .../public/actions/drilldown_grouping.ts | 8 +++- .../embeddable_enhanced/public/index.ts | 2 +- .../dynamic_action_grouping.ts | 23 +++++++++ .../dynamic_action_manager.test.ts | 22 +++++++++ .../dynamic_actions/dynamic_action_manager.ts | 2 + 17 files changed, 181 insertions(+), 54 deletions(-) create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md create mode 100644 x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md new file mode 100644 index 0000000000000..62610624655a1 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) + +## isContextMenuTriggerContext variable + +Signature: + +```typescript +isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md index df67eda5074b9..06f792837e4fe 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md @@ -77,6 +77,7 @@ | [contextMenuTrigger](./kibana-plugin-plugins-embeddable-public.contextmenutrigger.md) | | | [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | | | [EmbeddableRenderer](./kibana-plugin-plugins-embeddable-public.embeddablerenderer.md) | Helper react component to render an embeddable Can be used if you have an embeddable object or an embeddable factory Supports updating input by passing input prop | +| [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) | | | [isRangeSelectTriggerContext](./kibana-plugin-plugins-embeddable-public.israngeselecttriggercontext.md) | | | [isValueClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isvalueclicktriggercontext.md) | | | [PANEL\_BADGE\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_badge_trigger.md) | | diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 789353ca4abd7..0d2dcf208f2ef 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -70,6 +70,7 @@ export { isSavedObjectEmbeddableInput, isRangeSelectTriggerContext, isValueClickTriggerContext, + isContextMenuTriggerContext, EmbeddableStateTransfer, EmbeddableEditorState, EmbeddablePackageState, diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 54c7a2ecc129d..b2965b55dbdfa 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { Datatable } from '../../../../expressions'; import { Trigger } from '../../../../ui_actions/public'; import { IEmbeddable } from '..'; @@ -53,31 +54,49 @@ export type ChartActionContext = | ValueClickContext | RangeSelectContext; -export const isValueClickTriggerContext = ( - context: ChartActionContext -): context is ValueClickContext => context.data && 'data' in context.data; - -export const isRangeSelectTriggerContext = ( - context: ChartActionContext -): context is RangeSelectContext => context.data && 'range' in context.data; - export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { id: CONTEXT_MENU_TRIGGER, - title: 'Context menu', - description: 'Triggered on top-right corner context-menu select.', + title: i18n.translate('embeddableApi.contextMenuTrigger.title', { + defaultMessage: 'Context menu', + }), + description: i18n.translate('embeddableApi.contextMenuTrigger.description', { + defaultMessage: 'A panel top-right corner context menu click.', + }), }; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { id: PANEL_BADGE_TRIGGER, - title: 'Panel badges', - description: 'Actions appear in title bar when an embeddable loads in a panel.', + title: i18n.translate('embeddableApi.panelBadgeTrigger.title', { + defaultMessage: 'Panel badges', + }), + description: i18n.translate('embeddableApi.panelBadgeTrigger.description', { + defaultMessage: 'Actions appear in title bar when an embeddable loads in a panel.', + }), }; export const PANEL_NOTIFICATION_TRIGGER = 'PANEL_NOTIFICATION_TRIGGER'; export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = { id: PANEL_NOTIFICATION_TRIGGER, - title: 'Panel notifications', - description: 'Actions appear in top-right corner of a panel.', + title: i18n.translate('embeddableApi.panelNotificationTrigger.title', { + defaultMessage: 'Panel notifications', + }), + description: i18n.translate('embeddableApi.panelNotificationTrigger.description', { + defaultMessage: 'Actions appear in top-right corner of a panel.', + }), }; + +export const isValueClickTriggerContext = ( + context: ChartActionContext +): context is ValueClickContext => context.data && 'data' in context.data; + +export const isRangeSelectTriggerContext = ( + context: ChartActionContext +): context is RangeSelectContext => context.data && 'range' in context.data; + +export const isContextMenuTriggerContext = (context: unknown): context is EmbeddableContext => + !!context && + typeof context === 'object' && + !!(context as EmbeddableContext).embeddable && + typeof (context as EmbeddableContext).embeddable === 'object'; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 00971ed37db3a..e84dff1172c2e 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -695,6 +695,11 @@ export interface IEmbeddable): void; } +// Warning: (ae-missing-release-tag) "isContextMenuTriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext; + // Warning: (ae-missing-release-tag) "isErrorEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -884,7 +889,7 @@ export const withEmbeddableSubscription: { }); }); - test('not compatible if no triggers intersection', async () => { + test('not compatible if no triggers intersect', async () => { await assertNonCompatibility({ actionFactoriesTriggers: [], }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index a2192808c2d40..a417deb47db53 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -10,9 +10,12 @@ import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/pub import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { isEnhancedEmbeddable, - embeddableEnhancedContextMenuDrilldownGrouping, + embeddableEnhancedDrilldownGrouping, } from '../../../../../../embeddable_enhanced/public'; -import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + EmbeddableContext, +} from '../../../../../../../../src/plugins/embeddable/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; import { ensureNestedTriggers } from '../drilldown_shared'; @@ -27,7 +30,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType handle.close()} viewMode={'create'} dynamicActionManager={embeddable.enhancements.dynamicActions} - triggers={ensureNestedTriggers(embeddable.supportedTriggers())} + triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} /> ), diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 56ef25005078b..1f0570445a8fc 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -10,12 +10,16 @@ import { reactToUiComponent, toMountPoint, } from '../../../../../../../../src/plugins/kibana_react/public'; -import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { + EmbeddableContext, + ViewMode, + CONTEXT_MENU_TRIGGER, +} from '../../../../../../../../src/plugins/embeddable/public'; import { txtDisplayName } from './i18n'; import { MenuItem } from './menu_item'; import { isEnhancedEmbeddable, - embeddableEnhancedContextMenuDrilldownGrouping, + embeddableEnhancedDrilldownGrouping, } from '../../../../../../embeddable_enhanced/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; @@ -31,7 +35,7 @@ export class FlyoutEditDrilldownAction implements ActionByType handle.close()} viewMode={'manage'} dynamicActionManager={embeddable.enhancements.dynamicActions} - triggers={ensureNestedTriggers(embeddable.supportedTriggers())} + triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} /> ), diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx index 85e92d0827daa..807dfeed21d1f 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx @@ -6,7 +6,11 @@ import React from 'react'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; -import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { + ChartActionContext, + CONTEXT_MENU_TRIGGER, + IEmbeddable, +} from '../../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; import { SELECT_RANGE_TRIGGER, @@ -34,7 +38,10 @@ interface UrlDrilldownDeps { export type ActionContext = ChartActionContext; export type Config = UrlDrilldownConfig; -export type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER; +export type UrlTrigger = + | typeof CONTEXT_MENU_TRIGGER + | typeof VALUE_CLICK_TRIGGER + | typeof SELECT_RANGE_TRIGGER; export interface ActionFactoryContext extends BaseActionFactoryContext { embeddable?: IEmbeddable; } @@ -58,7 +65,7 @@ export class UrlDrilldown implements Drilldown = ({ diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts index 6989819da2b0b..a93e150deee8f 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts @@ -87,25 +87,25 @@ describe('VALUE_CLICK_TRIGGER', () => { ]) as ValueClickTriggerEventScope; expect(mockEventScope.points.length).toBeGreaterThan(3); expect(mockEventScope.points).toMatchInlineSnapshot(` - Array [ - Object { - "key": "event.points.0.key", - "value": "event.points.0.value", - }, - Object { - "key": "event.points.1.key", - "value": "event.points.1.value", - }, - Object { - "key": "event.points.2.key", - "value": "event.points.2.value", - }, - Object { - "key": "event.points.3.key", - "value": "event.points.3.value", - }, - ] - `); + Array [ + Object { + "key": "event.points.0.key", + "value": "event.points.0.value", + }, + Object { + "key": "event.points.1.key", + "value": "event.points.1.value", + }, + Object { + "key": "event.points.2.key", + "value": "event.points.2.value", + }, + Object { + "key": "event.points.3.key", + "value": "event.points.3.value", + }, + ] + `); }); }); @@ -130,3 +130,12 @@ describe('VALUE_CLICK_TRIGGER', () => { }); }); }); + +describe('CONTEXT_MENU_TRIGGER', () => { + test('getMockEventScope() results in empty scope', () => { + const mockEventScope = getMockEventScope([ + 'CONTEXT_MENU_TRIGGER', + ]) as ValueClickTriggerEventScope; + expect(mockEventScope).toEqual({}); + }); +}); diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts index 0f66cb144c967..234af380689e9 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts @@ -14,11 +14,15 @@ import { IEmbeddable, isRangeSelectTriggerContext, isValueClickTriggerContext, + isContextMenuTriggerContext, RangeSelectContext, ValueClickContext, } from '../../../../../../src/plugins/embeddable/public'; import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown'; -import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; type ContextScopeInput = ActionContext | ActionFactoryContext; @@ -101,7 +105,10 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld * URL drilldown event scope, * available as {{event.$}} */ -export type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope; +export type UrlDrilldownEventScope = + | ValueClickTriggerEventScope + | RangeSelectTriggerEventScope + | ContextMenuTriggerEventScope; export type EventScopeInput = ActionContext; export interface ValueClickTriggerEventScope { key?: string; @@ -115,11 +122,15 @@ export interface RangeSelectTriggerEventScope { to?: string | number; } +export type ContextMenuTriggerEventScope = object; + export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope { if (isRangeSelectTriggerContext(eventScopeInput)) { return getEventScopeFromRangeSelectTriggerContext(eventScopeInput); } else if (isValueClickTriggerContext(eventScopeInput)) { return getEventScopeFromValueClickTriggerContext(eventScopeInput); + } else if (isContextMenuTriggerContext(eventScopeInput)) { + return {}; } else { throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger"); } @@ -169,7 +180,9 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago to: new Date().toISOString(), }; - } else { + } + + if (trigger === VALUE_CLICK_TRIGGER) { // number of mock points to generate // should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER const nPoints = 4; @@ -184,6 +197,8 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco points, }; } + + return {}; } type Primitive = string | number | boolean | null; diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts index 5ea8928532c28..0aa1c0e6f08ae 100644 --- a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts +++ b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public'; -export const contextMenuDrilldownGrouping: PresentableGrouping<{ +export const drilldownGrouping: PresentableGrouping<{ embeddable?: IEmbeddable; }> = [ { id: 'drilldowns', - getDisplayName: () => 'Drilldowns', + getDisplayName: () => + i18n.translate('xpack.embeddableEnhanced.Drilldowns', { + defaultMessage: 'Drilldowns', + }), getIconType: () => 'symlink', order: 25, }, diff --git a/x-pack/plugins/embeddable_enhanced/public/index.ts b/x-pack/plugins/embeddable_enhanced/public/index.ts index a7916685239df..24f8eb623abe0 100644 --- a/x-pack/plugins/embeddable_enhanced/public/index.ts +++ b/x-pack/plugins/embeddable_enhanced/public/index.ts @@ -20,4 +20,4 @@ export function plugin(context: PluginInitializerContext) { export { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types'; export { isEnhancedEmbeddable } from './embeddables'; -export { contextMenuDrilldownGrouping as embeddableEnhancedContextMenuDrilldownGrouping } from './actions'; +export { drilldownGrouping as embeddableEnhancedDrilldownGrouping } from './actions'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts new file mode 100644 index 0000000000000..feda1c93e2511 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; +import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public'; + +export const dynamicActionGrouping: PresentableGrouping<{ + embeddable?: IEmbeddable; +}> = [ + { + id: 'dynamicActions', + getDisplayName: () => + i18n.translate('xpack.uiActionsEnhanced.CustomActions', { + defaultMessage: 'Custom actions', + }), + getIconType: () => 'symlink', + order: 26, + }, +]; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index cdd357f3560b8..cbc381c911c3d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -13,6 +13,7 @@ import { UiActionsServiceEnhancements } from '../services'; import { ActionFactoryDefinition } from './action_factory_definition'; import { SerializedAction, SerializedEvent } from './types'; import { licensingMock } from '../../../licensing/public/mocks'; +import { dynamicActionGrouping } from './dynamic_action_grouping'; const actionFactoryDefinition1: ActionFactoryDefinition = { id: 'ACTION_FACTORY_1', @@ -294,6 +295,27 @@ describe('DynamicActionManager', () => { expect(manager.state.get().events.length).toBe(1); }); + test('adds revived actiosn to "dynamic action" grouping', async () => { + const { manager, uiActions, actions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(manager.state.get().events.length).toBe(0); + + await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']); + + const createdAction = actions.values().next().value; + + expect(createdAction.grouping).toBe(dynamicActionGrouping); + }); + test('optimistically adds event to UI state', async () => { const { manager, uiActions } = setup([]); const action: SerializedAction = { diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index b414296690c9e..f096b17f8a78d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -18,6 +18,7 @@ import { } from '../../../../../src/plugins/kibana_utils/common'; import { StartContract } from '../plugin'; import { SerializedAction, SerializedEvent } from './types'; +import { dynamicActionGrouping } from './dynamic_action_grouping'; const compareEvents = ( a: ReadonlyArray<{ eventId: string }>, @@ -93,6 +94,7 @@ export class DynamicActionManager { uiActions.registerAction({ ...actionDefinition, id: actionId, + grouping: dynamicActionGrouping, isCompatible: async (context) => { if (!(await isCompatible(context))) return false; if (!actionDefinition.isCompatible) return true; From 81c0e126cc02290ac13595a705fb0f90cc992985 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Fri, 30 Oct 2020 08:28:28 -0400 Subject: [PATCH 64/73] [Fleet] fix duplicate ingest pipeline refs (#82078) * check if pipeline refs for version already exists * use the right version --- .../elasticsearch/ingest_pipeline/install.ts | 17 + .../apis/epm/install_remove_assets.ts | 414 ++++++++++-------- 2 files changed, 254 insertions(+), 177 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 43c0179c0aa8a..58abdeb0d443d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -14,6 +14,8 @@ import { import * as Registry from '../../registry'; import { CallESAsCurrentUser } from '../../../../types'; import { saveInstalledEsRefs } from '../../packages/install'; +import { getInstallationObject } from '../../packages'; +import { deletePipelineRefs } from './remove'; interface RewriteSubstitution { source: string; @@ -31,6 +33,7 @@ export const installPipelines = async ( // it can be created pointing to the new template, without removing the old one and effecting data // so do not remove the currently installed pipelines here const dataStreams = installablePackage.data_streams; + const { name: pkgName, version: pkgVersion } = installablePackage; if (!dataStreams?.length) return []; const pipelinePaths = paths.filter((path) => isPipeline(path)); // get and save pipeline refs before installing pipelines @@ -50,6 +53,20 @@ export const installPipelines = async ( acc.push(...pipelineObjectRefs); return acc; }, []); + + // check that we don't duplicate the pipeline refs if the user is reinstalling + const installedPkg = await getInstallationObject({ + savedObjectsClient, + pkgName, + }); + if (!installedPkg) throw new Error("integration wasn't found while installing pipelines"); + // remove the current pipeline refs, if any exist, associated with this version before saving new ones so no duplicates occur + await deletePipelineRefs( + savedObjectsClient, + installedPkg.attributes.installed_es, + pkgName, + pkgVersion + ); await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, pipelineRefs); const pipelines = dataStreams.reduce>>((acc, dataStream) => { if (dataStream.ingest_pipeline) { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index cc6a384dcaafe..72ea9cb4e7ef3 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -5,6 +5,8 @@ */ import expect from '@kbn/expect'; +import { sortBy } from 'lodash'; +import { AssetReference } from '../../../../plugins/ingest_manager/common'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -12,6 +14,8 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); const es = getService('es'); const pkgName = 'all_assets'; const pkgVersion = '0.1.0'; @@ -33,194 +37,27 @@ export default function (providerContext: FtrProviderContext) { describe('installs all assets when installing a package for the first time', async () => { skipIfNoDockerRegistry(providerContext); before(async () => { + if (!server.enabled) return; await installPackage(pkgKey); }); after(async () => { + if (!server.enabled) return; await uninstallPackage(pkgKey); }); - it('should have installed the ILM policy', async function () { - const resPolicy = await es.transport.request({ - method: 'GET', - path: `/_ilm/policy/all_assets`, - }); - expect(resPolicy.statusCode).equal(200); - }); - it('should have installed the index templates', async function () { - const resLogsTemplate = await es.transport.request({ - method: 'GET', - path: `/_index_template/${logsTemplateName}`, - }); - expect(resLogsTemplate.statusCode).equal(200); - - const resMetricsTemplate = await es.transport.request({ - method: 'GET', - path: `/_index_template/${metricsTemplateName}`, - }); - expect(resMetricsTemplate.statusCode).equal(200); - }); - it('should have installed the pipelines', async function () { - const res = await es.transport.request({ - method: 'GET', - path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, - }); - expect(res.statusCode).equal(200); - const resPipeline1 = await es.transport.request({ - method: 'GET', - path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, - }); - expect(resPipeline1.statusCode).equal(200); - const resPipeline2 = await es.transport.request({ - method: 'GET', - path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, - }); - expect(resPipeline2.statusCode).equal(200); - }); - it('should have installed the template components', async function () { - const res = await es.transport.request({ - method: 'GET', - path: `/_component_template/${logsTemplateName}-mappings`, - }); - expect(res.statusCode).equal(200); - const resSettings = await es.transport.request({ - method: 'GET', - path: `/_component_template/${logsTemplateName}-settings`, - }); - expect(resSettings.statusCode).equal(200); - }); - it('should have installed the transform components', async function () { - const res = await es.transport.request({ - method: 'GET', - path: `/_transform/${pkgName}.test-default-${pkgVersion}`, - }); - expect(res.statusCode).equal(200); - }); - it('should have created the index for the transform', async function () { - // the index is defined in the transform file - const res = await es.transport.request({ - method: 'GET', - path: `/logs-all_assets.test_log_current_default`, - }); - expect(res.statusCode).equal(200); - }); - it('should have installed the kibana assets', async function () { - const resIndexPatternLogs = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'logs-*', - }); - expect(resIndexPatternLogs.id).equal('logs-*'); - const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'metrics-*', - }); - expect(resIndexPatternMetrics.id).equal('metrics-*'); - const resDashboard = await kibanaServer.savedObjects.get({ - type: 'dashboard', - id: 'sample_dashboard', - }); - expect(resDashboard.id).equal('sample_dashboard'); - const resDashboard2 = await kibanaServer.savedObjects.get({ - type: 'dashboard', - id: 'sample_dashboard2', - }); - expect(resDashboard2.id).equal('sample_dashboard2'); - const resVis = await kibanaServer.savedObjects.get({ - type: 'visualization', - id: 'sample_visualization', - }); - expect(resVis.id).equal('sample_visualization'); - const resSearch = await kibanaServer.savedObjects.get({ - type: 'search', - id: 'sample_search', - }); - expect(resSearch.id).equal('sample_search'); - }); - it('should create an index pattern with the package fields', async () => { - const resIndexPatternLogs = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'logs-*', - }); - const fields = JSON.parse(resIndexPatternLogs.attributes.fields); - const exists = fields.find((field: { name: string }) => field.name === 'logs_test_name'); - expect(exists).not.to.be(undefined); - const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'metrics-*', - }); - const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); - const metricsExists = fieldsMetrics.find( - (field: { name: string }) => field.name === 'metrics_test_name' - ); - expect(metricsExists).not.to.be(undefined); - }); - it('should have created the correct saved object', async function () { - const res = await kibanaServer.savedObjects.get({ - type: 'epm-packages', - id: 'all_assets', - }); - expect(res.attributes).eql({ - installed_kibana: [ - { - id: 'sample_dashboard', - type: 'dashboard', - }, - { - id: 'sample_dashboard2', - type: 'dashboard', - }, - { - id: 'sample_search', - type: 'search', - }, - { - id: 'sample_visualization', - type: 'visualization', - }, - ], - installed_es: [ - { - id: 'logs-all_assets.test_logs-0.1.0', - type: 'ingest_pipeline', - }, - { - id: 'logs-all_assets.test_logs-0.1.0-pipeline1', - type: 'ingest_pipeline', - }, - { - id: 'logs-all_assets.test_logs-0.1.0-pipeline2', - type: 'ingest_pipeline', - }, - { - id: 'logs-all_assets.test_logs', - type: 'index_template', - }, - { - id: 'metrics-all_assets.test_metrics', - type: 'index_template', - }, - { - id: 'all_assets.test-default-0.1.0', - type: 'transform', - }, - ], - es_index_patterns: { - test_logs: 'logs-all_assets.test_logs-*', - test_metrics: 'metrics-all_assets.test_metrics-*', - }, - name: 'all_assets', - version: '0.1.0', - internal: false, - removable: true, - install_version: '0.1.0', - install_status: 'installed', - install_started_at: res.attributes.install_started_at, - install_source: 'registry', - }); + expectAssetsInstalled({ + logsTemplateName, + metricsTemplateName, + pkgVersion, + pkgName, + es, + kibanaServer, }); }); describe('uninstalls all assets when uninstalling a package', async () => { skipIfNoDockerRegistry(providerContext); before(async () => { + if (!server.enabled) return; // these tests ensure that uninstall works properly so make sure that the package gets installed and uninstalled // and then we'll test that not artifacts are left behind. await installPackage(pkgKey); @@ -403,5 +240,228 @@ export default function (providerContext: FtrProviderContext) { expect(res.response.data.statusCode).equal(404); }); }); + + describe('reinstalls all assets', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + if (!server.enabled) return; + await installPackage(pkgKey); + // reinstall + await installPackage(pkgKey); + }); + after(async () => { + if (!server.enabled) return; + await uninstallPackage(pkgKey); + }); + expectAssetsInstalled({ + logsTemplateName, + metricsTemplateName, + pkgVersion, + pkgName, + es, + kibanaServer, + }); + }); }); } + +const expectAssetsInstalled = ({ + logsTemplateName, + metricsTemplateName, + pkgVersion, + pkgName, + es, + kibanaServer, +}: { + logsTemplateName: string; + metricsTemplateName: string; + pkgVersion: string; + pkgName: string; + es: any; + kibanaServer: any; +}) => { + it('should have installed the ILM policy', async function () { + const resPolicy = await es.transport.request({ + method: 'GET', + path: `/_ilm/policy/all_assets`, + }); + expect(resPolicy.statusCode).equal(200); + }); + it('should have installed the index templates', async function () { + const resLogsTemplate = await es.transport.request({ + method: 'GET', + path: `/_index_template/${logsTemplateName}`, + }); + expect(resLogsTemplate.statusCode).equal(200); + + const resMetricsTemplate = await es.transport.request({ + method: 'GET', + path: `/_index_template/${metricsTemplateName}`, + }); + expect(resMetricsTemplate.statusCode).equal(200); + }); + it('should have installed the pipelines', async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, + }); + expect(res.statusCode).equal(200); + const resPipeline1 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, + }); + expect(resPipeline1.statusCode).equal(200); + const resPipeline2 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, + }); + expect(resPipeline2.statusCode).equal(200); + }); + it('should have installed the template components', async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}-mappings`, + }); + expect(res.statusCode).equal(200); + const resSettings = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}-settings`, + }); + expect(resSettings.statusCode).equal(200); + }); + it('should have installed the transform components', async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_transform/${pkgName}.test-default-${pkgVersion}`, + }); + expect(res.statusCode).equal(200); + }); + it('should have created the index for the transform', async function () { + // the index is defined in the transform file + const res = await es.transport.request({ + method: 'GET', + path: `/logs-all_assets.test_log_current_default`, + }); + expect(res.statusCode).equal(200); + }); + it('should have installed the kibana assets', async function () { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + expect(resIndexPatternLogs.id).equal('logs-*'); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + expect(resIndexPatternMetrics.id).equal('metrics-*'); + const resDashboard = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard', + }); + expect(resDashboard.id).equal('sample_dashboard'); + const resDashboard2 = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard2', + }); + expect(resDashboard2.id).equal('sample_dashboard2'); + const resVis = await kibanaServer.savedObjects.get({ + type: 'visualization', + id: 'sample_visualization', + }); + expect(resVis.id).equal('sample_visualization'); + const resSearch = await kibanaServer.savedObjects.get({ + type: 'search', + id: 'sample_search', + }); + expect(resSearch.id).equal('sample_search'); + }); + it('should create an index pattern with the package fields', async () => { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + const fields = JSON.parse(resIndexPatternLogs.attributes.fields); + const exists = fields.find((field: { name: string }) => field.name === 'logs_test_name'); + expect(exists).not.to.be(undefined); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + const metricsExists = fieldsMetrics.find( + (field: { name: string }) => field.name === 'metrics_test_name' + ); + expect(metricsExists).not.to.be(undefined); + }); + it('should have created the correct saved object', async function () { + const res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + // during a reinstall the items can change + const sortedRes = { + ...res.attributes, + installed_kibana: sortBy(res.attributes.installed_kibana, (o: AssetReference) => o.type), + installed_es: sortBy(res.attributes.installed_es, (o: AssetReference) => o.type), + }; + expect(sortedRes).eql({ + installed_kibana: [ + { + id: 'sample_dashboard', + type: 'dashboard', + }, + { + id: 'sample_dashboard2', + type: 'dashboard', + }, + { + id: 'sample_search', + type: 'search', + }, + { + id: 'sample_visualization', + type: 'visualization', + }, + ], + installed_es: [ + { + id: 'logs-all_assets.test_logs', + type: 'index_template', + }, + { + id: 'metrics-all_assets.test_metrics', + type: 'index_template', + }, + { + id: 'logs-all_assets.test_logs-0.1.0', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs-0.1.0-pipeline1', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs-0.1.0-pipeline2', + type: 'ingest_pipeline', + }, + { + id: 'all_assets.test-default-0.1.0', + type: 'transform', + }, + ], + es_index_patterns: { + test_logs: 'logs-all_assets.test_logs-*', + test_metrics: 'metrics-all_assets.test_metrics-*', + }, + name: 'all_assets', + version: '0.1.0', + internal: false, + removable: true, + install_version: '0.1.0', + install_status: 'installed', + install_started_at: res.attributes.install_started_at, + install_source: 'registry', + }); + }); +}; From 0bf3d6efb033f79c06707b738882864cdb534a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Fri, 30 Oct 2020 08:59:45 -0400 Subject: [PATCH 65/73] Add a link to documentation in the alerts and actions management UI (#81909) * Add a link to documentation in the alerts and actions management UI * Update label * Remove usage of any on registries --- .../public/application/app.tsx | 7 ++- .../context/actions_connectors_context.tsx | 5 +- .../application/context/alerts_context.tsx | 7 ++- .../public/application/home.test.tsx | 45 ++++++++++++++++++ .../public/application/home.tsx | 46 ++++++++++++------- .../action_connector_form.test.tsx | 2 +- .../action_connector_form.tsx | 5 +- .../action_form.test.tsx | 2 +- .../action_connector_form/action_form.tsx | 4 +- .../action_type_menu.test.tsx | 2 +- .../connector_add_flyout.test.tsx | 2 +- .../connector_add_modal.test.tsx | 2 +- .../connector_add_modal.tsx | 10 ++-- .../connector_edit_flyout.test.tsx | 2 +- .../actions_connectors_list.test.tsx | 2 +- .../sections/alert_form/alert_add.test.tsx | 4 +- .../sections/alert_form/alert_edit.test.tsx | 4 +- .../sections/alert_form/alert_form.test.tsx | 12 ++--- .../components/alerts_list.test.tsx | 12 ++--- .../public/application/test_utils/index.ts | 41 +++++++++++++++++ 20 files changed, 158 insertions(+), 58 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index c53dc0c105084..bb9fe65d6bbb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -18,8 +18,7 @@ import { } from 'kibana/public'; import { Section, routeToAlertDetails } from './constants'; import { AppContextProvider } from './app_context'; -import { ActionTypeModel, AlertTypeModel } from '../types'; -import { TypeRegistry } from './type_registry'; +import { ActionTypeRegistryContract, AlertTypeRegistryContract } from '../types'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerts/public'; @@ -42,8 +41,8 @@ export interface AppDeps { uiSettings: IUiSettingsClient; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; capabilities: ApplicationStart['capabilities']; - actionTypeRegistry: TypeRegistry; - alertTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; + alertTypeRegistry: AlertTypeRegistryContract; history: ScopedHistory; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx index 786fc12380f90..bb0606db2a9b3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx @@ -6,12 +6,11 @@ import React, { createContext, useContext } from 'react'; import { HttpSetup, ApplicationStart, DocLinksStart, ToastsSetup } from 'kibana/public'; -import { ActionTypeModel, ActionConnector } from '../../types'; -import { TypeRegistry } from '../type_registry'; +import { ActionTypeRegistryContract, ActionConnector } from '../../types'; export interface ActionsConnectorsContextValue { http: HttpSetup; - actionTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsSetup; capabilities: ApplicationStart['capabilities']; reloadConnectors?: () => Promise; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx index b4cf13538d64d..a4293f94268ba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx @@ -18,14 +18,13 @@ import { DataPublicPluginStartUi, IndexPatternsContract, } from 'src/plugins/data/public'; -import { TypeRegistry } from '../type_registry'; -import { AlertTypeModel, ActionTypeModel } from '../../types'; +import { AlertTypeRegistryContract, ActionTypeRegistryContract } from '../../types'; export interface AlertsContextValue> { reloadAlerts?: () => Promise; http: HttpSetup; - alertTypeRegistry: TypeRegistry; - actionTypeRegistry: TypeRegistry; + alertTypeRegistry: AlertTypeRegistryContract; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsStart; uiSettings?: IUiSettingsClient; charts?: ChartsPluginSetup; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx new file mode 100644 index 0000000000000..d19dc3d303479 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { RouteComponentProps, Router } from 'react-router-dom'; +import { createMemoryHistory, createLocation } from 'history'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import TriggersActionsUIHome, { MatchParams } from './home'; +import { AppContextProvider } from './app_context'; +import { getMockedAppDependencies } from './test_utils'; + +describe('home', () => { + it('renders the documentation link', async () => { + const deps = await getMockedAppDependencies(); + + const props: RouteComponentProps = { + history: createMemoryHistory(), + location: createLocation('/'), + match: { + isExact: true, + path: `/alerts`, + url: '', + params: { + section: 'alerts', + }, + }, + }; + const wrapper = mountWithIntl( + + + + + + ); + const documentationLink = wrapper.find('[data-test-subj="documentationLink"]'); + expect(documentationLink.exists()).toBeTruthy(); + expect(documentationLink.first().prop('href')).toEqual( + 'https://www.elastic.co/guide/en/kibana/mocked-test-branch/managing-alerts-and-actions.html' + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 482b38ffc0d68..450f33d4f7e89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -10,13 +10,14 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, EuiSpacer, EuiTab, EuiTabs, EuiTitle, EuiText, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { Section, routeToConnectors, routeToAlerts } from './constants'; @@ -30,7 +31,7 @@ import { AlertsList } from './sections/alerts_list/components/alerts_list'; import { HealthCheck } from './components/health_check'; import { HealthContextProvider } from './context/health_context'; -interface MatchParams { +export interface MatchParams { section: Section; } @@ -80,27 +81,40 @@ export const TriggersActionsUIHome: React.FunctionComponent - - - + + +

-
- - -

+ + + -

-
-
-
+ + + + + + +

+ +

+
{tabs.map((tab) => ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index 60ec8004983a3..cf83062b5781e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -22,7 +22,7 @@ describe('action_connector_form', () => { ] = await mocks.getStartServices(); deps = { http: mocks.http, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index f91bd7382b61c..3a1f9872a96a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -24,10 +24,9 @@ import { ReducerAction } from './connector_reducer'; import { ActionConnector, IErrorObject, - ActionTypeModel, + ActionTypeRegistryContract, UserConfiguredActionConnector, } from '../../../types'; -import { TypeRegistry } from '../../type_registry'; import { hasSaveActionsCapability } from '../../lib/capabilities'; export function validateBaseProperties(actionObject: ActionConnector) { @@ -61,7 +60,7 @@ interface ActionConnectorProps< }; errors: IErrorObject; http: HttpSetup; - actionTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; docLinks: DocLinksStart; capabilities: ApplicationStart['capabilities']; consumer?: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 3e229c6a2333d..7c718e8248e41 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -178,7 +178,7 @@ describe('action_form', () => { }, }, setHasActionsWithBrokenConnector: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; actionTypeRegistry.list.mockReturnValue([ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 61cf3f2d37925..51d3b0074ca54 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -34,6 +34,7 @@ import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/act import { IErrorObject, ActionTypeModel, + ActionTypeRegistryContract, AlertAction, ActionTypeIndex, ActionConnector, @@ -42,7 +43,6 @@ import { } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; -import { TypeRegistry } from '../../type_registry'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; @@ -55,7 +55,7 @@ interface ActionAccordionFormProps { setAlertProperty: (actions: AlertAction[]) => void; setActionParamsProperty: (key: string, value: any, index: number) => void; http: HttpSetup; - actionTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsSetup; docLinks: DocLinksStart; actionTypes?: ActionType[]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index a5e9cdc65cfa6..2fe068536f4a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -33,7 +33,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 0863465833c0b..b32a8ed4161d6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -34,7 +34,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index 3d621367fc40a..cba9eea3cf3f7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -32,7 +32,7 @@ describe('connector_add_modal', () => { delete: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index d7ca91218d4dd..13ec8395aa557 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -19,12 +19,16 @@ import { EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; -import { ActionType, ActionConnector, IErrorObject, ActionTypeModel } from '../../../types'; import { connectorReducer } from './connector_reducer'; import { createActionConnector } from '../../lib/action_connector_api'; -import { TypeRegistry } from '../../type_registry'; import './connector_add_modal.scss'; import { hasSaveActionsCapability } from '../../lib/capabilities'; +import { + ActionType, + ActionConnector, + IErrorObject, + ActionTypeRegistryContract, +} from '../../../types'; interface ConnectorAddModalProps { actionType: ActionType; @@ -32,7 +36,7 @@ interface ConnectorAddModalProps { setAddModalVisibility: React.Dispatch>; postSaveEventHandler?: (savedAction: ActionConnector) => void; http: HttpSetup; - actionTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: Pick< ToastsApi, 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 0c2f4df0ca52b..ac379e279f7f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -35,7 +35,7 @@ describe('connector_edit_flyout', () => { show: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, alertTypeRegistry: {} as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index c96e62df71ce4..33b839dc70b31 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -69,7 +69,7 @@ describe('actions_connectors_list component empty', () => { }, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, alertTypeRegistry: {} as any, }; actionTypeRegistry.has.mockReturnValue(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 8ac80c4ad2880..0ac20626e1044 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -84,8 +84,8 @@ describe('alert_add', () => { uiSettings: mocks.uiSettings, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 24eb7aabb9549..fe86e5da98765 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -36,8 +36,8 @@ describe('alert_edit', () => { toastNotifications: mockedCoreSetup.notifications.toasts, http: mockedCoreSetup.http, uiSettings: mockedCoreSetup.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 6091519f5851e..cda791489d7f7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -98,8 +98,8 @@ describe('alert_form', () => { toastNotifications: mocks.notifications.toasts, http: mocks.http, uiSettings: mocks.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; @@ -231,8 +231,8 @@ describe('alert_form', () => { toastNotifications: mocks.notifications.toasts, http: mocks.http, uiSettings: mocks.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; @@ -332,8 +332,8 @@ describe('alert_form', () => { toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, }; alertTypeRegistry.list.mockReturnValue([alertType]); alertTypeRegistry.get.mockReturnValue(alertType); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 86b9afd9565f8..e6e44d4d21bdf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -109,8 +109,8 @@ describe('alerts_list component empty', () => { capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, }; wrapper = mountWithIntl( @@ -278,8 +278,8 @@ describe('alerts_list component with items', () => { capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, }; alertTypeRegistry.has.mockReturnValue(true); @@ -478,8 +478,8 @@ describe('alerts_list with show only capability', () => { capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, }; alertTypeRegistry.has.mockReturnValue(false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts new file mode 100644 index 0000000000000..7b3872246ca50 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { alertingPluginMock } from '../../../../alerts/public/mocks'; +import { actionTypeRegistryMock } from '../action_type_registry.mock'; +import { alertTypeRegistryMock } from '../alert_type_registry.mock'; +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; + +export async function getMockedAppDependencies() { + const coreSetupMock = coreMock.createSetup(); + const actionTypeRegistry = actionTypeRegistryMock.create(); + const alertTypeRegistry = alertTypeRegistryMock.create(); + const [ + { + chrome, + docLinks, + application: { capabilities, navigateToApp }, + }, + ] = await coreSetupMock.getStartServices(); + return { + chrome, + docLinks, + dataPlugin: dataPluginMock.createStartContract(), + charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), + toastNotifications: coreSetupMock.notifications.toasts, + http: coreSetupMock.http, + uiSettings: coreSetupMock.uiSettings, + navigateToApp, + capabilities, + history: scopedHistoryMock.create(), + setBreadcrumbs: jest.fn(), + actionTypeRegistry, + alertTypeRegistry, + }; +} From b10979c35e2a8e5b591d3e4211fea40191e2a5e3 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 30 Oct 2020 08:14:13 -0500 Subject: [PATCH 66/73] skip 'returns a single bucket if array has 1'. related #81460 --- .../server/lib/requests/__tests__/get_ping_histogram.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index 0ae5887b31a7b..ac940ffb6676f 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -35,7 +35,7 @@ describe('getPingHistogram', () => { }, }; - it('returns a single bucket if array has 1', async () => { + it.skip('returns a single bucket if array has 1', async () => { expect.assertions(2); const mockEsClient = jest.fn(); mockEsClient.mockReturnValue({ From 415a063dd08a700fdf56d56ad73f1a78914021fb Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 30 Oct 2020 15:17:19 +0200 Subject: [PATCH 67/73] [Graph] Fix problem with duplicate ids (#82109) --- .../graph/public/components/field_manager/field_editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx index f4006d6bf142b..ec5da1f44223c 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx @@ -116,7 +116,7 @@ export function FieldEditor({ return ( Date: Fri, 30 Oct 2020 08:38:08 -0500 Subject: [PATCH 68/73] TS project references for share plugin (#82051) --- src/plugins/share/tsconfig.json | 15 ++++++++++ tsconfig.json | 2 ++ tsconfig.refs.json | 1 + .../tsconfig.json | 1 + x-pack/test/tsconfig.json | 30 +++++++------------ x-pack/tsconfig.json | 1 + 6 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 src/plugins/share/tsconfig.json diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json new file mode 100644 index 0000000000000..a6318af602b4d --- /dev/null +++ b/src/plugins/share/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../../plugins/kibana_utils/tsconfig.json" } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 30b38d0fc2dd3..2e4d5730d9320 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", "src/plugins/newsfeed/**/*", + "src/plugins/share/**/*", "src/plugins/telemetry_collection_manager/**/*", "src/plugins/telemetry/**/*", "src/plugins/url_forwarding/**/*", @@ -31,6 +32,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index c16e7a5e1b0f1..a37aa7b5b57fb 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -8,6 +8,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index f8c1a6b53dac5..05e5f39d4d628 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -16,5 +16,6 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" } ] } diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 7bd38ea4afab7..e041292ebf3c9 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -3,30 +3,22 @@ "compilerOptions": { // overhead is too significant "incremental": false, - "types": [ - "mocha", - "node", - "flot" - ] + "types": ["mocha", "node", "flot"] }, - "include": [ - "**/*", - "../typings/**/*" - ], - "exclude": [ - "../typings/jest.d.ts" - ], + "include": ["**/*", "../typings/**/*"], + "exclude": ["../typings/jest.d.ts"], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../plugins/licensing/tsconfig.json" }, - { "path": "../plugins/global_search/tsconfig.json" }, - { "path": "../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../../src/plugins/share/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, - { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "../../src/plugins/newsfeed/tsconfig.json" } + { "path": "../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/licensing/tsconfig.json" }, + { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 5c76a11315a56..804268fbf5dac 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -28,6 +28,7 @@ { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../src/plugins/share/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/url_forwarding/tsconfig.json" }, From bf73bcf5d5773ea098f3f147a771a0b2aaf09713 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 30 Oct 2020 14:38:20 +0100 Subject: [PATCH 69/73] add tests for index pattern switching (#81987) --- .../apps/lens/persistent_context.ts | 7 ++++ .../test/functional/apps/lens/smokescreen.ts | 6 +++ .../es_archives/lens/basic/data.json.gz | Bin 4623 -> 4844 bytes .../test/functional/page_objects/lens_page.ts | 38 ++++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index 8d536aac3f795..a115b720f6f2c 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -67,6 +67,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.hasFilter('ip', '97.220.3.248', false, true); }); + it('keeps selected index pattern after refresh', async () => { + await PageObjects.lens.switchDataPanelIndexPattern('otherpattern'); + await browser.refresh(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDataPanelIndexPattern()).to.equal('otherpattern'); + }); + it('keeps time range and pinned filters after refreshing directly after saving', async () => { // restore defaults so visualization becomes saveable await security.testUser.restoreDefaults(); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 6c4fa94a259e9..0ddafe581c21d 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -308,5 +308,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getDatatableHeaderText(1)).to.eql('Average of bytes'); expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('6,011.351'); }); + + it('should allow to change index pattern', async () => { + await PageObjects.lens.switchFirstLayerIndexPattern('otherpattern'); + expect(await PageObjects.lens.getFirstLayerIndexPattern()).to.equal('otherpattern'); + expect(await PageObjects.lens.isShowingNoResults()).to.equal(true); + }); }); } diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json.gz b/x-pack/test/functional/es_archives/lens/basic/data.json.gz index ddf4a27289dffdaf1c605b0db01cfa9554a2b209..c9ae08fe6f6281d8f1e6066738dab36afd700f93 100644 GIT binary patch literal 4844 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 E0NnCaPXGV_ literal 4623 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 Date: Fri, 30 Oct 2020 14:12:11 +0000 Subject: [PATCH 70/73] [Security Solution] Modal for saving timeline (#81802) * init modal for saving timeline * disable auto save * unit test * fix type error * update translation * add unit tests * rename constant * break components into files * autoFocus and close modal on finish * rename constant * fix description label * update wording * review * fix dependency * remove classname * update wording Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/common/constants.ts | 2 + .../components/flyout/header/index.tsx | 22 +- .../header/save_timeline_button.test.tsx | 76 +++++ .../timeline/header/save_timeline_button.tsx | 87 ++++++ .../header/title_and_description.test.tsx | 261 ++++++++++++++++++ .../timeline/header/title_and_description.tsx | 218 +++++++++++++++ .../timeline/header/translations.ts | 64 +++++ .../timeline/properties/helpers.test.tsx | 73 ++++- .../timeline/properties/helpers.tsx | 191 ++++++++++--- .../timeline/properties/index.test.tsx | 1 + .../components/timeline/properties/index.tsx | 1 + .../timeline/properties/properties_left.tsx | 4 + .../components/timeline/properties/styles.tsx | 26 +- .../timeline/properties/translations.ts | 9 +- .../properties/use_create_timeline.test.tsx | 40 +++ .../properties/use_create_timeline.tsx | 19 +- .../timelines/store/timeline/actions.ts | 24 +- .../public/timelines/store/timeline/epic.ts | 6 +- .../timelines/store/timeline/reducer.ts | 2 +- 19 files changed, 1064 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2910f02a187f4..767a2616a4c7e 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -179,3 +179,5 @@ export const showAllOthersBucket: string[] = [ 'destination.ip', 'user.name', ]; + +export const ENABLE_NEW_TIMELINE = false; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index a711e7a1d0442..0737db7a00788 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -125,13 +125,27 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - updateDescription: ({ id, description }: { id: string; description: string }) => - dispatch(timelineActions.updateDescription({ id, description })), + updateDescription: ({ + id, + description, + disableAutoSave, + }: { + id: string; + description: string; + disableAutoSave?: boolean; + }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })), updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) => dispatch(timelineActions.updateIsFavorite({ id, isFavorite })), updateNote: (note: Note) => dispatch(appActions.updateNote({ note })), - updateTitle: ({ id, title }: { id: string; title: string }) => - dispatch(timelineActions.updateTitle({ id, title })), + updateTitle: ({ + id, + title, + disableAutoSave, + }: { + id: string; + title: string; + disableAutoSave?: boolean; + }) => dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })), toggleLock: ({ linkToId }: { linkToId: InputsModelId }) => dispatch(inputsActions.toggleTimelineLinkTo({ linkToId })), }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx new file mode 100644 index 0000000000000..e9dc312ee8d19 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { SaveTimelineButton } from './save_timeline_button'; +import { act } from '@testing-library/react-hooks'; + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + }; +}); + +jest.mock('./title_and_description'); + +describe('SaveTimelineButton', () => { + const props = { + timelineId: 'timeline-1', + showOverlay: false, + toolTip: 'tooltip message', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + }; + test('Show tooltip', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true); + }); + + test('Hide tooltip', () => { + const testProps = { + ...props, + showOverlay: true, + }; + const component = mount(); + component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click'); + + act(() => { + expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual( + false + ); + }); + }); + + test('should show a button with pencil icon', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-button-icon"]').prop('iconType')).toEqual( + 'pencil' + ); + }); + + test('should not show a modal when showOverlay equals false', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(false); + }); + + test('should show a modal when showOverlay equals true', () => { + const testProps = { + ...props, + showOverlay: true, + }; + const component = mount(); + component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click'); + act(() => { + expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx new file mode 100644 index 0000000000000..476ef8d1dd5a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon, EuiOverlayMask, EuiModal, EuiToolTip } from '@elastic/eui'; + +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { timelineActions } from '../../../store/timeline'; +import { NOTES_PANEL_WIDTH } from '../properties/notes_size'; + +import { TimelineTitleAndDescription } from './title_and_description'; +import { EDIT } from './translations'; + +export interface SaveTimelineComponentProps { + timelineId: string; + toolTip?: string; +} + +export const SaveTimelineButton = React.memo( + ({ timelineId, toolTip }) => { + const [showSaveTimelineOverlay, setShowSaveTimelineOverlay] = useState(false); + const onToggleSaveTimeline = useCallback(() => { + setShowSaveTimelineOverlay((prevShowSaveTimelineOverlay) => !prevShowSaveTimelineOverlay); + }, [setShowSaveTimelineOverlay]); + + const dispatch = useDispatch(); + const updateTitle = useCallback( + ({ id, title, disableAutoSave }: { id: string; title: string; disableAutoSave?: boolean }) => + dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })), + [dispatch] + ); + + const updateDescription = useCallback( + ({ + id, + description, + disableAutoSave, + }: { + id: string; + description: string; + disableAutoSave?: boolean; + }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })), + [dispatch] + ); + + const saveTimelineButtonIcon = useMemo( + () => ( + + ), + [onToggleSaveTimeline] + ); + + return showSaveTimelineOverlay ? ( + <> + {saveTimelineButtonIcon} + + + + + + + ) : ( + + {saveTimelineButtonIcon} + + ); + } +); + +SaveTimelineButton.displayName = 'SaveTimelineButton'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx new file mode 100644 index 0000000000000..bcc90a25d5789 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TimelineTitleAndDescription } from './title_and_description'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { useCreateTimelineButton } from '../properties/use_create_timeline'; +import { TimelineType } from '../../../../../common/types/timeline'; +import * as i18n from './translations'; + +jest.mock('../../../../common/hooks/use_selector', () => ({ + useShallowEqualSelector: jest.fn(), +})); + +jest.mock('../../../../timelines/store/timeline', () => ({ + timelineSelectors: { + selectTimeline: jest.fn(), + }, +})); + +jest.mock('../properties/use_create_timeline', () => ({ + useCreateTimelineButton: jest.fn(), +})); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + }; +}); + +describe('TimelineTitleAndDescription', () => { + describe('save timeline', () => { + const props = { + timelineId: 'timeline-1', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + }; + + const mockGetButton = jest.fn().mockReturnValue(
); + + beforeEach(() => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: '', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.default, + }); + (useCreateTimelineButton as jest.Mock).mockReturnValue({ + getButton: mockGetButton, + }); + }); + + afterEach(() => { + (useShallowEqualSelector as jest.Mock).mockReset(); + (useCreateTimelineButton as jest.Mock).mockReset(); + mockGetButton.mockClear(); + }); + + test('show proress bar while saving', () => { + const component = shallow(); + expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); + }); + + test('Show correct header for save timeline modal', () => { + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.SAVE_TIMELINE + ); + }); + + test('Show correct header for save timeline template modal', () => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: '', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.template, + }); + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.SAVE_TIMELINE_TEMPLATE + ); + }); + + test('Show name field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true); + }); + + test('Show description field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true); + }); + + test('Show close button', () => { + const component = shallow(); + expect(component.find('[data-test-subj="close-button"]').exists()).toEqual(true); + }); + + test('Show saveButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); + }); + }); + + describe('update timeline', () => { + const props = { + timelineId: 'timeline-1', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + }; + + const mockGetButton = jest.fn().mockReturnValue(
); + + beforeEach(() => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: 'xxxx', + isSaving: true, + savedObjectId: '1234', + title: 'my timeline', + timelineType: TimelineType.default, + }); + (useCreateTimelineButton as jest.Mock).mockReturnValue({ + getButton: mockGetButton, + }); + }); + + afterEach(() => { + (useShallowEqualSelector as jest.Mock).mockReset(); + (useCreateTimelineButton as jest.Mock).mockReset(); + mockGetButton.mockClear(); + }); + + test('show proress bar while saving', () => { + const component = shallow(); + expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); + }); + + test('Show correct header for save timeline modal', () => { + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.NAME_TIMELINE + ); + }); + + test('Show correct header for save timeline template modal', () => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: 'xxxx', + isSaving: true, + savedObjectId: '1234', + title: 'my timeline', + timelineType: TimelineType.template, + }); + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.NAME_TIMELINE_TEMPLATE + ); + }); + + test('Show name field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true); + }); + + test('Show description field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true); + }); + + test('Show saveButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); + }); + }); + + describe('showWarning', () => { + const props = { + timelineId: 'timeline-1', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + showWarning: true, + }; + + const mockGetButton = jest.fn().mockReturnValue(
); + + beforeEach(() => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: '', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.default, + showWarnging: true, + }); + (useCreateTimelineButton as jest.Mock).mockReturnValue({ + getButton: mockGetButton, + }); + }); + + afterEach(() => { + (useShallowEqualSelector as jest.Mock).mockReset(); + (useCreateTimelineButton as jest.Mock).mockReset(); + mockGetButton.mockClear(); + }); + + test('Show EuiCallOut', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-callout"]').exists()).toEqual(true); + }); + + test('Show discardTimelineButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="mock-discard-button"]').exists()).toEqual(true); + }); + + test('get discardTimelineButton with correct props', () => { + shallow(); + expect(mockGetButton).toBeCalledWith({ + title: i18n.DISCARD_TIMELINE, + outline: true, + iconType: '', + fill: false, + }); + }); + + test('get discardTimelineTemplateButton with correct props', () => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: 'xxxx', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.template, + }); + shallow(); + expect(mockGetButton).toBeCalledWith({ + title: i18n.DISCARD_TIMELINE_TEMPLATE, + outline: true, + iconType: '', + fill: false, + }); + }); + + test('Show saveButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx new file mode 100644 index 0000000000000..3597b26e2663a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFormRow, + EuiFlexItem, + EuiModalBody, + EuiModalHeader, + EuiSpacer, + EuiProgress, + EuiCallOut, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { TimelineType } from '../../../../../common/types/timeline'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; +import { TimelineInput } from '../../../store/timeline/actions'; +import { Description, Name, UpdateTitle, UpdateDescription } from '../properties/helpers'; +import { TIMELINE_TITLE, DESCRIPTION, OPTIONAL } from '../properties/translations'; +import { useCreateTimelineButton } from '../properties/use_create_timeline'; +import * as i18n from './translations'; + +interface TimelineTitleAndDescriptionProps { + showWarning?: boolean; + timelineId: string; + toggleSaveTimeline: () => void; + updateTitle: UpdateTitle; + updateDescription: UpdateDescription; +} + +const Wrapper = styled(EuiModalBody)` + .euiFormRow { + max-width: none; + } + + .euiFormControlLayout { + max-width: none; + } + + .euiFieldText { + max-width: none; + } +`; + +Wrapper.displayName = 'Wrapper'; + +const usePrevious = (value: unknown) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; + +// when showWarning equals to true, +// the modal is used as a reminder for users to save / discard +// the unsaved timeline / template +export const TimelineTitleAndDescription = React.memo( + ({ timelineId, toggleSaveTimeline, updateTitle, updateDescription, showWarning }) => { + const timeline = useShallowEqualSelector((state) => + timelineSelectors.selectTimeline(state, timelineId) + ); + + const { description, isSaving, savedObjectId, title, timelineType } = timeline; + + const prevIsSaving = usePrevious(isSaving); + const dispatch = useDispatch(); + const onSaveTimeline = useCallback( + (args: TimelineInput) => dispatch(timelineActions.saveTimeline(args)), + [dispatch] + ); + + const handleClick = useCallback(() => { + onSaveTimeline({ + ...timeline, + id: timelineId, + }); + }, [onSaveTimeline, timeline, timelineId]); + + const { getButton } = useCreateTimelineButton({ timelineId, timelineType }); + + const discardTimelineButton = useMemo( + () => + getButton({ + title: + timelineType === TimelineType.template + ? i18n.DISCARD_TIMELINE_TEMPLATE + : i18n.DISCARD_TIMELINE, + outline: true, + iconType: '', + fill: false, + }), + [getButton, timelineType] + ); + + useEffect(() => { + if (!isSaving && prevIsSaving) { + toggleSaveTimeline(); + } + }, [isSaving, prevIsSaving, toggleSaveTimeline]); + + const modalHeader = + savedObjectId == null + ? timelineType === TimelineType.template + ? i18n.SAVE_TIMELINE_TEMPLATE + : i18n.SAVE_TIMELINE + : timelineType === TimelineType.template + ? i18n.NAME_TIMELINE_TEMPLATE + : i18n.NAME_TIMELINE; + + const saveButtonTitle = + savedObjectId == null && showWarning + ? timelineType === TimelineType.template + ? i18n.SAVE_TIMELINE_TEMPLATE + : i18n.SAVE_TIMELINE + : i18n.SAVE; + + const calloutMessage = useMemo(() => i18n.UNSAVED_TIMELINE_WARNING(timelineType), [ + timelineType, + ]); + + const descriptionLabel = savedObjectId == null ? `${DESCRIPTION} (${OPTIONAL})` : DESCRIPTION; + + return ( + <> + {isSaving && ( + + )} + {modalHeader} + + + {showWarning && ( + + + + + )} + + + + + + + + + + + + + + + + {savedObjectId == null && showWarning ? ( + discardTimelineButton + ) : ( + + {i18n.CLOSE_MODAL} + + )} + + + + {saveButtonTitle} + + + + + + + ); + } +); + +TimelineTitleAndDescription.displayName = 'TimelineTitleAndDescription'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts index 89ad11d75cae1..80aa719a3469d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { TimelineType, TimelineTypeLiteral } from '../../../../../common/types/timeline'; export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate( 'xpack.securitySolution.timeline.callOut.unauthorized.message.description', @@ -21,3 +22,66 @@ export const CALL_OUT_IMMUTABLE = i18n.translate( 'This prebuilt timeline template cannot be modified. To make changes, please duplicate this template and make modifications to the duplicate template.', } ); + +export const EDIT = i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.button', { + defaultMessage: 'edit', +}); + +export const SAVE_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.header', + { + defaultMessage: 'Save Timeline', + } +); + +export const SAVE_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.header', + { + defaultMessage: 'Save Timeline Template', + } +); + +export const SAVE = i18n.translate('xpack.securitySolution.timeline.nameTimeline.save.title', { + defaultMessage: 'Save', +}); + +export const NAME_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.nameTimeline.modal.header', + { + defaultMessage: 'Name Timeline', + } +); + +export const NAME_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.nameTimelineTemplate.modal.header', + { + defaultMessage: 'Name Timeline Template', + } +); + +export const DISCARD_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.discard.title', + { + defaultMessage: 'Discard Timeline', + } +); + +export const DISCARD_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title', + { + defaultMessage: 'Discard Timeline Template', + } +); + +export const CLOSE_MODAL = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.close.title', + { + defaultMessage: 'Close', + } +); + +export const UNSAVED_TIMELINE_WARNING = (timelineType: TimelineTypeLiteral) => + i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.warning.title', { + values: { timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline' }, + defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?', + }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx index 887c2e1e825f8..dd0695e795397 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx @@ -5,8 +5,10 @@ */ import React from 'react'; import { mount, shallow } from 'enzyme'; -import { NewTimeline, NewTimelineProps } from './helpers'; +import { Description, Name, NewTimeline, NewTimelineProps } from './helpers'; import { useCreateTimelineButton } from './use_create_timeline'; +import * as i18n from './translations'; +import { TimelineType } from '../../../../../common/types/timeline'; jest.mock('./use_create_timeline', () => ({ useCreateTimelineButton: jest.fn(), @@ -83,3 +85,72 @@ describe('NewTimeline', () => { }); }); }); + +describe('Description', () => { + const props = { + description: 'xxx', + timelineId: 'timeline-1', + updateDescription: jest.fn(), + }; + + test('should render tooltip', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="timeline-description-tool-tip"]').prop('content') + ).toEqual(i18n.DESCRIPTION_TOOL_TIP); + }); + + test('should not render textarea if isTextArea is false', () => { + const component = shallow(); + expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( + false + ); + + expect(component.find('[data-test-subj="timeline-description"]').exists()).toEqual(true); + }); + + test('should render textarea if isTextArea is true', () => { + const testProps = { + ...props, + isTextArea: true, + }; + const component = shallow(); + expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( + true + ); + }); +}); + +describe('Name', () => { + const props = { + timelineId: 'timeline-1', + timelineType: TimelineType.default, + title: 'xxx', + updateTitle: jest.fn(), + }; + + test('should render tooltip', () => { + const component = shallow(); + expect(component.find('[data-test-subj="timeline-title-tool-tip"]').prop('content')).toEqual( + i18n.TITLE + ); + }); + + test('should render placeholder by timelineType - timeline', () => { + const component = shallow(); + expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual( + i18n.UNTITLED_TIMELINE + ); + }); + + test('should render placeholder by timelineType - timeline template', () => { + const testProps = { + ...props, + timelineType: TimelineType.template, + }; + const component = shallow(); + expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual( + i18n.UNTITLED_TEMPLATE + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index a28f4240d3a2f..25039dbc9529a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -16,8 +16,9 @@ import { EuiModal, EuiOverlayMask, EuiToolTip, + EuiTextArea, } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -41,14 +42,22 @@ import { Notes } from '../../notes'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; import { NOTES_PANEL_WIDTH } from './notes_size'; -import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; +import { + ButtonContainer, + DescriptionContainer, + LabelText, + NameField, + NameWrapper, + StyledStar, +} from './styles'; import * as i18n from './translations'; -import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; +import { setInsertTimeline, showTimeline, TimelineInput } from '../../../store/timeline/actions'; import { useCreateTimelineButton } from './use_create_timeline'; export const historyToolTip = 'The chronological history of actions related to this timeline'; export const streamLiveToolTip = 'Update the Timeline as new data arrives'; export const newTimelineToolTip = 'Create a new timeline'; +export const TIMELINE_TITLE_CLASSNAME = 'timeline-title'; const NotesCountBadge = (styled(EuiBadge)` margin-left: 5px; @@ -66,8 +75,25 @@ type CreateTimeline = ({ timelineType?: TimelineTypeLiteral; }) => void; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; +export type UpdateTitle = ({ + id, + title, + disableAutoSave, +}: { + id: string; + title: string; + disableAutoSave?: boolean; +}) => void; +export type UpdateDescription = ({ + id, + description, + disableAutoSave, +}: { + id: string; + description: string; + disableAutoSave?: boolean; +}) => void; +export type SaveTimeline = (args: TimelineInput) => void; export const StarIcon = React.memo<{ isFavorite: boolean; @@ -104,55 +130,146 @@ interface DescriptionProps { description: string; timelineId: string; updateDescription: UpdateDescription; + isTextArea?: boolean; + disableAutoSave?: boolean; + disableTooltip?: boolean; + disabled?: boolean; + marginRight?: number; } export const Description = React.memo( - ({ description, timelineId, updateDescription }) => ( - - - updateDescription({ id: timelineId, description: e.target.value })} - placeholder={i18n.DESCRIPTION} - spellCheck={true} - value={description} - /> + ({ + description, + timelineId, + updateDescription, + isTextArea = false, + disableAutoSave = false, + disableTooltip = false, + disabled = false, + marginRight, + }) => { + const onDescriptionChanged = useCallback( + (e) => { + updateDescription({ id: timelineId, description: e.target.value, disableAutoSave }); + }, + [updateDescription, disableAutoSave, timelineId] + ); + + const inputField = useMemo( + () => + isTextArea ? ( + + ) : ( + + ), + [description, isTextArea, onDescriptionChanged, disabled] + ); + return ( + + {disableTooltip ? ( + inputField + ) : ( + + {inputField} + + )} - - ) + ); + } ); Description.displayName = 'Description'; interface NameProps { + autoFocus?: boolean; + disableAutoSave?: boolean; + disableTooltip?: boolean; + disabled?: boolean; timelineId: string; timelineType: TimelineType; title: string; updateTitle: UpdateTitle; + width?: string; + marginRight?: number; } -export const Name = React.memo(({ timelineId, timelineType, title, updateTitle }) => { - const handleChange = useCallback((e) => updateTitle({ id: timelineId, title: e.target.value }), [ +export const Name = React.memo( + ({ + autoFocus = false, + disableAutoSave = false, + disableTooltip = false, + disabled = false, timelineId, + timelineType, + title, updateTitle, - ]); + width, + marginRight, + }) => { + const timelineNameRef = useRef(null); + + const handleChange = useCallback( + (e) => updateTitle({ id: timelineId, title: e.target.value, disableAutoSave }), + [timelineId, updateTitle, disableAutoSave] + ); - return ( - - - - ); -}); + useEffect(() => { + if (autoFocus && timelineNameRef && timelineNameRef.current) { + timelineNameRef.current.focus(); + } + }, [autoFocus]); + + const nameField = useMemo( + () => ( + + ), + [handleChange, marginRight, timelineType, title, width, disabled] + ); + + return ( + + {disableTooltip ? ( + nameField + ) : ( + + {nameField} + + )} + + ); + } +); Name.displayName = 'Name'; interface NewCaseProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index 19344a7fd7c9b..cdedca23e85af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -92,6 +92,7 @@ const defaultProps = { description: '', getNotesByIds: jest.fn(), noteIds: [], + saveTimeline: jest.fn(), status: TimelineStatus.active, timelineId: 'abc', toggleLock: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index 9eea95a0a9b1a..9df2b585449a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -92,6 +92,7 @@ export const Properties = React.memo( setShowTimelineModal(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); const datePickerWidth = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx index a3cd8802c36bc..6b181a5af7bf3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx @@ -16,6 +16,8 @@ import { SuperDatePicker } from '../../../../common/components/super_date_picker import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline'; import * as i18n from './translations'; +import { SaveTimelineButton } from '../header/save_timeline_button'; +import { ENABLE_NEW_TIMELINE } from '../../../../../common/constants'; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; @@ -122,6 +124,8 @@ export const PropertiesLeft = React.memo( ) : null} + {ENABLE_NEW_TIMELINE && } + {showNotesFromWidth ? ( (({ width }) => ({ `; DatePicker.displayName = 'DatePicker'; -export const NameField = styled(EuiFieldText)` - width: 150px; - margin-right: 5px; +export const NameField = styled(({ width, marginRight, ...rest }) => )` + width: ${({ width = '150px' }) => width}; + margin-right: ${({ marginRight = 10 }) => marginRight} px; + + .euiToolTipAnchor { + display: block; + } `; NameField.displayName = 'NameField'; -export const DescriptionContainer = styled.div` +export const NameWrapper = styled.div` + .euiToolTipAnchor { + display: block; + } +`; +NameWrapper.displayName = 'NameWrapper'; + +export const DescriptionContainer = styled.div<{ marginRight?: number }>` animation: ${fadeInEffect} 0.3s; - margin-right: 5px; + margin-right: ${({ marginRight = 5 }) => marginRight}px; min-width: 150px; + + .euiToolTipAnchor { + display: block; + } `; DescriptionContainer.displayName = 'DescriptionContainer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts index 1fc3b7b00f847..78d01b2d98ab3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts @@ -34,7 +34,7 @@ export const NOT_A_FAVORITE = i18n.translate( export const TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.timeline.properties.timelineTitleAriaLabel', { - defaultMessage: 'Timeline title', + defaultMessage: 'Title', } ); @@ -194,3 +194,10 @@ export const UNLOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( defaultMessage: 'Unlock date picker to global date picker', } ); + +export const OPTIONAL = i18n.translate( + 'xpack.securitySolution.timeline.properties.timelineDescriptionOptional', + { + defaultMessage: 'Optional', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index c21592bed12e0..10b505da5c76f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -63,6 +63,46 @@ describe('useCreateTimelineButton', () => { }); }); + test('getButton renders correct iconType - EuiButton', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } + ); + await waitForNextUpdate(); + + const button = result.current.getButton({ + outline: true, + title: 'mock title', + iconType: 'pencil', + }); + const wrapper = shallow(button); + expect(wrapper.find('[data-test-subj="timeline-new-with-border"]').prop('iconType')).toEqual( + 'pencil' + ); + }); + }); + + test('getButton renders correct filling - EuiButton', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } + ); + await waitForNextUpdate(); + + const button = result.current.getButton({ + outline: true, + title: 'mock title', + fill: false, + }); + const wrapper = shallow(button); + expect(wrapper.find('[data-test-subj="timeline-new-with-border"]').prop('fill')).toEqual( + false + ); + }); + }); + test('getButton renders correct outline - EuiButtonEmpty', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index 28dd865c763ae..b4d168cc980b6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -93,15 +93,28 @@ export const useCreateTimelineButton = ({ }, [createTimeline, timelineId, timelineType, closeGearMenu]); const getButton = useCallback( - ({ outline, title }: { outline?: boolean; title?: string }) => { + ({ + outline, + title, + iconType = 'plusInCircle', + fill = true, + isDisabled = false, + }: { + outline?: boolean; + title?: string; + iconType?: string; + fill?: boolean; + isDisabled?: boolean; + }) => { const buttonProps = { - iconType: 'plusInCircle', + iconType, onClick: handleButtonClick, + fill, }; const dataTestSubjPrefix = timelineType === TimelineType.template ? `template-timeline-new` : `timeline-new`; return outline ? ( - + {title} ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 472e82426468e..c066de8af9f20 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -56,7 +56,7 @@ export const applyDeltaToColumnWidth = actionCreator<{ delta: number; }>('APPLY_DELTA_TO_COLUMN_WIDTH'); -export const createTimeline = actionCreator<{ +export interface TimelineInput { id: string; dataProviders?: DataProvider[]; dateRange?: { @@ -76,9 +76,13 @@ export const createTimeline = actionCreator<{ sort?: Sort; showCheckboxes?: boolean; timelineType?: TimelineTypeLiteral; - templateTimelineId?: string; - templateTimelineVersion?: number; -}>('CREATE_TIMELINE'); + templateTimelineId?: string | null; + templateTimelineVersion?: number | null; +} + +export const saveTimeline = actionCreator('SAVE_TIMELINE'); + +export const createTimeline = actionCreator('CREATE_TIMELINE'); export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); @@ -174,9 +178,11 @@ export const updateHighlightedDropAndProviderId = actionCreator<{ providerId: string; }>('UPDATE_DROP_AND_PROVIDER'); -export const updateDescription = actionCreator<{ id: string; description: string }>( - 'UPDATE_DESCRIPTION' -); +export const updateDescription = actionCreator<{ + id: string; + description: string; + disableAutoSave?: boolean; +}>('UPDATE_DESCRIPTION'); export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); @@ -205,7 +211,9 @@ export const updateItemsPerPageOptions = actionCreator<{ itemsPerPageOptions: number[]; }>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); -export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); +export const updateTitle = actionCreator<{ id: string; title: string; disableAutoSave?: boolean }>( + 'UPDATE_TITLE' +); export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( 'UPDATE_PAGE_INDEX' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index cc8e856de1b16..d50de33412175 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -78,6 +78,7 @@ import { createTimeline, addTimeline, showCallOutUnauthorizedMsg, + saveTimeline, } from './actions'; import { ColumnHeaderOptions, TimelineModel } from './model'; import { epicPersistNote, timelineNoteActionsType } from './epic_note'; @@ -95,6 +96,7 @@ const timelineActionsType = [ dataProviderEdited.type, removeColumn.type, removeProvider.type, + saveTimeline.type, setExcludedRowRendererIds.type, setFilters.type, setSavedQueryId.type, @@ -179,11 +181,11 @@ export const createTimelineEpic = (): Epic< } else if ( timelineActionsType.includes(action.type) && !timelineObj.isLoading && - isItAtimelineAction(timelineId) + isItAtimelineAction(timelineId) && + !get('payload.disableAutoSave', action) ) { return true; } - return false; }), debounceTime(500), mergeMap(([action]) => { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 1d956e02e7083..7c227f1c80610 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -389,7 +389,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: updateTimelineKqlMode({ id, kqlMode, timelineById: state.timelineById }), })) - .case(updateTitle, (state, { id, title }) => ({ + .case(updateTitle, (state, { id, title, disableAutoSave }) => ({ ...state, timelineById: updateTimelineTitle({ id, title, timelineById: state.timelineById }), })) From 70807c98bd989f9715279e76567407c8012c7692 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 30 Oct 2020 16:45:24 +0200 Subject: [PATCH 71/73] [Actions] Fix actionType type on registerType function (#82125) --- x-pack/plugins/actions/server/plugin.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index d0c7bf350504b..06898f6688037 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -254,9 +254,10 @@ export class ActionsPlugin implements Plugin, Plugi registerType: < Config extends ActionTypeConfig = ActionTypeConfig, Secrets extends ActionTypeSecrets = ActionTypeSecrets, - Params extends ActionTypeParams = ActionTypeParams + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void >( - actionType: ActionType + actionType: ActionType ) => { if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); From c1294f0177bddb8764e61445b0242b7da0f2fcc8 Mon Sep 17 00:00:00 2001 From: igoristic Date: Fri, 30 Oct 2020 10:50:34 -0400 Subject: [PATCH 72/73] [Monitoring] Thread pool rejections alert (#79433) * Thread pool rejections first draft * Split search and write rejections to seperate alerts * Code review feedback * Optimized page loading and bundle size * Increased monitoring bundle limit * Removed server app import into the frontend * Fixed tests and bundle size Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- x-pack/plugins/monitoring/common/constants.ts | 177 +++++++++- x-pack/plugins/monitoring/common/enums.ts | 1 + x-pack/plugins/monitoring/common/types.ts | 53 --- .../types.d.ts => common/types/alerts.ts} | 77 ++++- .../monitoring/public/alerts/badge.tsx | 4 +- .../monitoring/public/alerts/callout.tsx | 4 +- .../alerts/components/duration/expression.tsx | 20 +- .../cpu_usage_alert/cpu_usage_alert.tsx | 9 +- .../public/alerts/disk_usage_alert/index.tsx | 10 +- .../public/alerts/filter_alert_states.ts | 2 +- .../alert_param_duration.tsx | 2 +- .../flyout_expressions/alert_param_number.tsx | 36 ++ .../alerts/legacy_alert/legacy_alert.tsx | 11 +- .../public/alerts/lib/replace_tokens.tsx | 2 +- .../alerts/lib/should_show_alert_badge.ts | 2 +- .../alerts/memory_usage_alert/index.tsx | 10 +- .../expression.tsx | 4 +- .../missing_monitoring_data_alert.tsx | 12 +- .../monitoring/public/alerts/panel.tsx | 3 +- .../monitoring/public/alerts/status.tsx | 3 +- .../thread_pool_rejections_alert/index.tsx | 60 ++++ .../public/angular/providers/private.js | 8 +- .../public/components/chart/chart_target.js | 12 +- .../chart/timeseries_visualization.js | 24 +- .../cluster/overview/elasticsearch_panel.js | 4 + .../components/elasticsearch/nodes/nodes.js | 8 +- .../shard_allocation/components/unassigned.js | 4 +- .../lib/has_primary_children.js | 4 +- .../shard_allocation/lib/vents.js | 6 +- .../transformers/indices_by_nodes.js | 17 +- .../transformers/nodes_by_indices.js | 30 +- .../plugins/monitoring/public/legacy_shims.ts | 1 - .../public/lib/calculate_shard_stats.js | 8 +- .../public/lib/get_cluster_from_clusters.js | 6 +- .../monitoring/public/lib/route_init.js | 3 +- x-pack/plugins/monitoring/public/plugin.ts | 61 +++- .../monitoring/public/services/features.js | 6 +- .../monitoring/public/services/title.js | 4 +- .../elasticsearch/node/advanced/index.js | 4 + .../public/views/elasticsearch/node/index.js | 4 + .../public/views/elasticsearch/nodes/index.js | 4 + .../{alerts_common.ts => alert_helpers.ts} | 2 +- .../server/alerts/alerts_factory.test.ts | 5 - .../server/alerts/alerts_factory.ts | 8 +- .../monitoring/server/alerts/base_alert.ts | 23 +- .../server/alerts/cluster_health_alert.ts | 12 +- .../server/alerts/cpu_usage_alert.ts | 47 +-- .../server/alerts/disk_usage_alert.ts | 42 +-- .../elasticsearch_version_mismatch_alert.ts | 16 +- .../plugins/monitoring/server/alerts/index.ts | 4 +- .../alerts/kibana_version_mismatch_alert.ts | 16 +- .../server/alerts/license_expiration_alert.ts | 11 +- .../alerts/logstash_version_mismatch_alert.ts | 16 +- .../server/alerts/memory_usage_alert.ts | 42 +-- .../alerts/missing_monitoring_data_alert.ts | 37 +-- .../server/alerts/nodes_changed_alert.ts | 12 +- .../thread_pool_rejections_alert_base.ts | 312 ++++++++++++++++++ .../thread_pool_search_rejections_alert.ts | 26 ++ .../thread_pool_write_rejections_alert.ts | 26 ++ .../server/lib/alerts/fetch_clusters.ts | 2 +- .../lib/alerts/fetch_cpu_usage_node_stats.ts | 2 +- .../lib/alerts/fetch_disk_usage_node_stats.ts | 2 +- .../server/lib/alerts/fetch_legacy_alerts.ts | 2 +- .../alerts/fetch_memory_usage_node_stats.ts | 2 +- .../alerts/fetch_missing_monitoring_data.ts | 2 +- .../server/lib/alerts/fetch_status.test.ts | 2 +- .../server/lib/alerts/fetch_status.ts | 8 +- .../fetch_thread_pool_rejections_stats.ts | 141 ++++++++ .../server/routes/api/v1/alerts/status.ts | 2 +- 70 files changed, 1149 insertions(+), 395 deletions(-) delete mode 100644 x-pack/plugins/monitoring/common/types.ts rename x-pack/plugins/monitoring/{server/alerts/types.d.ts => common/types/alerts.ts} (66%) create mode 100644 x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_number.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx rename x-pack/plugins/monitoring/server/alerts/{alerts_common.ts => alert_helpers.ts} (97%) create mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3f9fdb164e759..770bb4f510301 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -54,7 +54,7 @@ pageLoadAssetSize: mapsLegacy: 116817 mapsLegacyLicensing: 20214 ml: 82187 - monitoring: 268612 + monitoring: 50000 navigation: 37269 newsfeed: 42228 observability: 89709 diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 76d9e7517b6ab..8ff8381d702e8 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; +import { CommonAlertParamDetail } from './types/alerts'; +import { AlertParamType } from './enums'; + /** * Helper string to add as a tag in every logging call */ @@ -215,15 +219,6 @@ export const REPORTING_SYSTEM_ID = 'reporting'; */ export const TELEMETRY_COLLECTION_INTERVAL = 86400000; -/** - * We want to slowly rollout the migration from watcher-based cluster alerts to - * kibana alerts and we only want to enable the kibana alerts once all - * watcher-based cluster alerts have been migrated so this flag will serve - * as the only way to see the new UI and actually run Kibana alerts. It will - * be false until all alerts have been migrated, then it will be removed - */ -export const KIBANA_CLUSTER_ALERTS_ENABLED = false; - /** * The prefix for all alert types used by monitoring */ @@ -238,6 +233,168 @@ export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_versio export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`; export const ALERT_MEMORY_USAGE = `${ALERT_PREFIX}alert_jvm_memory_usage`; export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monitoring_data`; +export const ALERT_THREAD_POOL_SEARCH_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_search_rejections`; +export const ALERT_THREAD_POOL_WRITE_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_write_rejections`; + +/** + * Legacy alerts details/label for server and public use + */ +export const LEGACY_ALERT_DETAILS = { + [ALERT_CLUSTER_HEALTH]: { + label: i18n.translate('xpack.monitoring.alerts.clusterHealth.label', { + defaultMessage: 'Cluster health', + }), + }, + [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: { + label: i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', { + defaultMessage: 'Elasticsearch version mismatch', + }), + }, + [ALERT_KIBANA_VERSION_MISMATCH]: { + label: i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', { + defaultMessage: 'Kibana version mismatch', + }), + }, + [ALERT_LICENSE_EXPIRATION]: { + label: i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', { + defaultMessage: 'License expiration', + }), + }, + [ALERT_LOGSTASH_VERSION_MISMATCH]: { + label: i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', { + defaultMessage: 'Logstash version mismatch', + }), + }, + [ALERT_NODES_CHANGED]: { + label: i18n.translate('xpack.monitoring.alerts.nodesChanged.label', { + defaultMessage: 'Nodes changed', + }), + }, +}; + +/** + * Alerts details/label for server and public use + */ +export const ALERT_DETAILS = { + [ALERT_CPU_USAGE]: { + label: i18n.translate('xpack.monitoring.alerts.cpuUsage.label', { + defaultMessage: 'CPU Usage', + }), + paramDetails: { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label', { + defaultMessage: `Notify when CPU is over`, + }), + type: AlertParamType.Percentage, + } as CommonAlertParamDetail, + duration: { + label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label', { + defaultMessage: `Look at the average over`, + }), + type: AlertParamType.Duration, + } as CommonAlertParamDetail, + }, + }, + [ALERT_DISK_USAGE]: { + paramDetails: { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label', { + defaultMessage: `Notify when disk capacity is over`, + }), + type: AlertParamType.Percentage, + }, + duration: { + label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.duration.label', { + defaultMessage: `Look at the average over`, + }), + type: AlertParamType.Duration, + }, + }, + label: i18n.translate('xpack.monitoring.alerts.diskUsage.label', { + defaultMessage: 'Disk Usage', + }), + }, + [ALERT_MEMORY_USAGE]: { + paramDetails: { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label', { + defaultMessage: `Notify when memory usage is over`, + }), + type: AlertParamType.Percentage, + }, + duration: { + label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label', { + defaultMessage: `Look at the average over`, + }), + type: AlertParamType.Duration, + }, + }, + label: i18n.translate('xpack.monitoring.alerts.memoryUsage.label', { + defaultMessage: 'Memory Usage (JVM)', + }), + }, + [ALERT_MISSING_MONITORING_DATA]: { + paramDetails: { + duration: { + label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', { + defaultMessage: `Notify if monitoring data is missing for the last`, + }), + type: AlertParamType.Duration, + } as CommonAlertParamDetail, + limit: { + label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', { + defaultMessage: `looking back`, + }), + type: AlertParamType.Duration, + } as CommonAlertParamDetail, + }, + label: i18n.translate('xpack.monitoring.alerts.missingData.label', { + defaultMessage: 'Missing monitoring data', + }), + }, + [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: { + paramDetails: { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.threshold.label', { + defaultMessage: `Notify when {type} rejection count is over`, + values: { type: 'search' }, + }), + type: AlertParamType.Number, + }, + duration: { + label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.duration.label', { + defaultMessage: `In the last`, + }), + type: AlertParamType.Duration, + }, + }, + label: i18n.translate('xpack.monitoring.alerts.threadPoolRejections.label', { + defaultMessage: 'Thread pool {type} rejections', + values: { type: 'search' }, + }), + }, + [ALERT_THREAD_POOL_WRITE_REJECTIONS]: { + paramDetails: { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.threshold.label', { + defaultMessage: `Notify when {type} rejection count is over`, + values: { type: 'write' }, + }), + type: AlertParamType.Number, + }, + duration: { + label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.duration.label', { + defaultMessage: `In the last`, + }), + type: AlertParamType.Duration, + }, + }, + label: i18n.translate('xpack.monitoring.alerts.threadPoolRejections.label', { + defaultMessage: 'Thread pool {type} rejections', + values: { type: 'write' }, + }), + }, +}; /** * A listing of all alert types @@ -253,6 +410,8 @@ export const ALERTS = [ ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MEMORY_USAGE, ALERT_MISSING_MONITORING_DATA, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ]; /** diff --git a/x-pack/plugins/monitoring/common/enums.ts b/x-pack/plugins/monitoring/common/enums.ts index d4058e9de801e..b373428bb279b 100644 --- a/x-pack/plugins/monitoring/common/enums.ts +++ b/x-pack/plugins/monitoring/common/enums.ts @@ -25,6 +25,7 @@ export enum AlertMessageTokenType { export enum AlertParamType { Duration = 'duration', Percentage = 'percentage', + Number = 'number', } export enum SetupModeFeature { diff --git a/x-pack/plugins/monitoring/common/types.ts b/x-pack/plugins/monitoring/common/types.ts deleted file mode 100644 index 825d2e454b3bb..0000000000000 --- a/x-pack/plugins/monitoring/common/types.ts +++ /dev/null @@ -1,53 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { Alert } from '../../alerts/common'; -import { AlertParamType } from './enums'; - -export interface CommonBaseAlert { - type: string; - label: string; - paramDetails: CommonAlertParamDetails; - rawAlert: Alert; - isLegacy: boolean; -} - -export interface CommonAlertStatus { - exists: boolean; - enabled: boolean; - states: CommonAlertState[]; - alert: CommonBaseAlert; -} - -export interface CommonAlertState { - firing: boolean; - state: any; - meta: any; -} - -export interface CommonAlertFilter { - nodeUuid?: string; -} - -export interface CommonAlertNodeUuidFilter extends CommonAlertFilter { - nodeUuid: string; -} - -export interface CommonAlertStackProductFilter extends CommonAlertFilter { - stackProduct: string; -} - -export interface CommonAlertParamDetail { - label: string; - type: AlertParamType; -} - -export interface CommonAlertParamDetails { - [name: string]: CommonAlertParamDetail; -} - -export interface CommonAlertParams { - [name: string]: string | number; -} diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/common/types/alerts.ts similarity index 66% rename from x-pack/plugins/monitoring/server/alerts/types.d.ts rename to x-pack/plugins/monitoring/common/types/alerts.ts index 0b346e770a299..f7a27a1b1a2b0 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -3,8 +3,60 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; -import { AlertInstanceState as BaseAlertInstanceState } from '../../../alerts/server'; + +import { Alert } from '../../../alerts/common'; +import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums'; + +export interface CommonBaseAlert { + type: string; + label: string; + paramDetails: CommonAlertParamDetails; + rawAlert: Alert; + isLegacy: boolean; +} + +export interface CommonAlertStatus { + exists: boolean; + enabled: boolean; + states: CommonAlertState[]; + alert: CommonBaseAlert; +} + +export interface CommonAlertState { + firing: boolean; + state: any; + meta: any; +} + +export interface CommonAlertFilter { + nodeUuid?: string; +} + +export interface CommonAlertNodeUuidFilter extends CommonAlertFilter { + nodeUuid: string; +} + +export interface CommonAlertStackProductFilter extends CommonAlertFilter { + stackProduct: string; +} + +export interface CommonAlertParamDetail { + label: string; + type?: AlertParamType; +} + +export interface CommonAlertParamDetails { + [name: string]: CommonAlertParamDetail | undefined; +} + +export interface CommonAlertParams { + [name: string]: string | number; +} + +export interface ThreadPoolRejectionsAlertParams { + threshold: number; + duration: string; +} export interface AlertEnableAction { id: string; @@ -12,7 +64,9 @@ export interface AlertEnableAction { } export interface AlertInstanceState { - alertStates: Array; + alertStates: Array< + AlertState | AlertCpuUsageState | AlertDiskUsageState | AlertThreadPoolRejectionsState + >; [x: string]: unknown; } @@ -46,6 +100,13 @@ export interface AlertMemoryUsageState extends AlertNodeState { memoryUsage: number; } +export interface AlertThreadPoolRejectionsState extends AlertState { + rejectionCount: number; + type: string; + nodeId: string; + nodeName?: string; +} + export interface AlertUiState { isFiring: boolean; severity: AlertSeverity; @@ -100,6 +161,14 @@ export interface AlertCpuUsageNodeStats extends AlertNodeStats { containerQuota: number; } +export interface AlertThreadPoolRejectionsStats { + clusterUuid: string; + nodeId: string; + nodeName: string; + rejectionCount: number; + ccs?: string; +} + export interface AlertDiskUsageNodeStats extends AlertNodeStats { diskUsage: number; } @@ -121,7 +190,7 @@ export interface AlertData { instanceKey: string; clusterUuid: string; ccs?: string; - shouldFire: boolean; + shouldFire?: boolean; severity: AlertSeverity; meta: any; } diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index d4e823a194f8e..4bfecf4380d4b 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -14,11 +14,11 @@ import { EuiFlexItem, EuiText, } from '@elastic/eui'; -import { CommonAlertStatus, CommonAlertState } from '../../common/types'; +import { CommonAlertStatus, CommonAlertState } from '../../common/types/alerts'; import { AlertSeverity } from '../../common/enums'; // @ts-ignore import { formatDateTimeLocal } from '../../common/formatting'; -import { AlertMessage, AlertState } from '../../server/alerts/types'; +import { AlertMessage, AlertState } from '../../common/types/alerts'; import { AlertPanel } from './panel'; import { Legacy } from '../legacy_shims'; import { isInSetupMode } from '../lib/setup_mode'; diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index 1ddd41c268456..769d4dc7b256d 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -7,10 +7,10 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { CommonAlertStatus } from '../../common/types'; +import { CommonAlertStatus } from '../../common/types/alerts'; import { AlertSeverity } from '../../common/enums'; import { replaceTokens } from './lib/replace_tokens'; -import { AlertMessage, AlertState } from '../../server/alerts/types'; +import { AlertMessage, AlertState } from '../../common/types/alerts'; const TYPES = [ { diff --git a/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx b/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx index 2df7169efc675..26593fdd6e7b0 100644 --- a/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx @@ -6,10 +6,11 @@ import React, { Fragment } from 'react'; import { EuiForm, EuiSpacer } from '@elastic/eui'; -import { CommonAlertParamDetails } from '../../../../common/types'; +import { CommonAlertParamDetails } from '../../../../common/types/alerts'; import { AlertParamDuration } from '../../flyout_expressions/alert_param_duration'; import { AlertParamType } from '../../../../common/enums'; import { AlertParamPercentage } from '../../flyout_expressions/alert_param_percentage'; +import { AlertParamNumber } from '../../flyout_expressions/alert_param_number'; export interface Props { alertParams: { [property: string]: any }; @@ -26,14 +27,14 @@ export const Expression: React.FC = (props) => { const details = paramDetails[alertParamName]; const value = alertParams[alertParamName]; - switch (details.type) { + switch (details?.type) { case AlertParamType.Duration: return ( @@ -43,12 +44,23 @@ export const Expression: React.FC = (props) => { ); + case AlertParamType.Number: + return ( + + ); } }); diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index fb4ecacf57fd6..d15fe6344ec0f 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -6,20 +6,17 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { ALERT_CPU_USAGE } from '../../../common/constants'; +import { ALERT_CPU_USAGE, ALERT_DETAILS } from '../../../common/constants'; import { validate } from '../components/duration/validation'; import { Expression, Props } from '../components/duration/expression'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { CpuUsageAlert } from '../../../server/alerts'; export function createCpuUsageAlertType(): AlertTypeModel { - const alert = new CpuUsageAlert(); return { id: ALERT_CPU_USAGE, - name: alert.label, + name: ALERT_DETAILS[ALERT_CPU_USAGE].label, iconClass: 'bell', alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index c2abb35612b38..589b374cae32c 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -10,17 +10,15 @@ import { Expression, Props } from '../components/duration/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DiskUsageAlert } from '../../../server/alerts'; +import { ALERT_DISK_USAGE, ALERT_DETAILS } from '../../../common/constants'; export function createDiskUsageAlertType(): AlertTypeModel { return { - id: DiskUsageAlert.TYPE, - name: DiskUsageAlert.LABEL, + id: ALERT_DISK_USAGE, + name: ALERT_DETAILS[ALERT_DISK_USAGE].label, iconClass: 'bell', alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts index 63714a6921e3f..e13ea7de0e226 100644 --- a/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts +++ b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CommonAlertState, CommonAlertStatus } from '../../common/types'; +import { CommonAlertState, CommonAlertStatus } from '../../common/types/alerts'; export function filterAlertStates( alerts: { [type: string]: CommonAlertStatus }, diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx index 862f32efd7361..4ece1b0c81827 100644 --- a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx @@ -69,7 +69,7 @@ export const AlertParamDuration: React.FC = (props: Props) => { }, [unit, value]); return ( - 0}> + 0}> void; +} +export const AlertParamNumber: React.FC = (props: Props) => { + const { name, label, setAlertParams, errors } = props; + const [value, setValue] = useState(props.value); + return ( + 0}> + { + let newValue = Number(e.target.value); + if (isNaN(newValue)) { + newValue = 0; + } + setValue(newValue); + setAlertParams(name, newValue); + }} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index f6223d41ab30e..83201b0512dbb 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -3,24 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTextColor, EuiSpacer } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { LEGACY_ALERTS } from '../../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BY_TYPE } from '../../../server/alerts'; +import { LEGACY_ALERTS, LEGACY_ALERT_DETAILS } from '../../../common/constants'; export function createLegacyAlertTypes(): AlertTypeModel[] { return LEGACY_ALERTS.map((legacyAlert) => { - const alertCls = BY_TYPE[legacyAlert]; - const alert = new alertCls(); return { id: legacyAlert, - name: alert.label, + name: LEGACY_ALERT_DETAILS[legacyAlert].label, iconClass: 'bell', - alertParamsExpression: (props: any) => ( + alertParamsExpression: () => ( diff --git a/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx index 02f5703f66382..b8ac69cbae68a 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx @@ -11,7 +11,7 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertMessageDocLinkToken, -} from '../../../server/alerts/types'; +} from '../../../common/types/alerts'; // @ts-ignore import { formatTimestampToDuration } from '../../../common'; import { CALCULATE_DURATION_UNTIL } from '../../../common/constants'; diff --git a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts index 0b95592d92c84..2ec5d1ba8f94a 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts +++ b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { isInSetupMode } from '../../lib/setup_mode'; -import { CommonAlertStatus } from '../../../common/types'; +import { CommonAlertStatus } from '../../../common/types/alerts'; import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context'; export function shouldShowAlertBadge( diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index dd60967a3458b..d3d48d907d02e 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -10,17 +10,15 @@ import { Expression, Props } from '../components/duration/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MemoryUsageAlert } from '../../../server/alerts'; +import { ALERT_MEMORY_USAGE, ALERT_DETAILS } from '../../../common/constants'; export function createMemoryUsageAlertType(): AlertTypeModel { return { - id: MemoryUsageAlert.TYPE, - name: MemoryUsageAlert.LABEL, + id: ALERT_MEMORY_USAGE, + name: ALERT_DETAILS[ALERT_MEMORY_USAGE].label, iconClass: 'bell', alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx index 7dc6155de529e..ac30a02173a5c 100644 --- a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { EuiForm, EuiSpacer } from '@elastic/eui'; -import { CommonAlertParamDetails } from '../../../common/types'; +import { CommonAlertParamDetails } from '../../../common/types/alerts'; import { AlertParamDuration } from '../flyout_expressions/alert_param_duration'; import { AlertParamType } from '../../../common/enums'; import { AlertParamPercentage } from '../flyout_expressions/alert_param_percentage'; @@ -26,7 +26,7 @@ export const Expression: React.FC = (props) => { const details = paramDetails[alertParamName]; const value = alertParams[alertParamName]; - switch (details.type) { + switch (details?.type) { case AlertParamType.Duration: return ( ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx index eb3b6ff9da1be..99db6c8b3c945 100644 --- a/x-pack/plugins/monitoring/public/alerts/panel.tsx +++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx @@ -18,8 +18,7 @@ import { EuiListGroupItem, } from '@elastic/eui'; -import { CommonAlertStatus, CommonAlertState } from '../../common/types'; -import { AlertMessage } from '../../server/alerts/types'; +import { CommonAlertStatus, CommonAlertState, AlertMessage } from '../../common/types/alerts'; import { Legacy } from '../legacy_shims'; import { replaceTokens } from './lib/replace_tokens'; import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx index c1ad41fc8d763..53918807a4272 100644 --- a/x-pack/plugins/monitoring/public/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/alerts/status.tsx @@ -7,9 +7,8 @@ import React from 'react'; import { EuiToolTip, EuiHealth } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { CommonAlertStatus } from '../../common/types'; +import { CommonAlertStatus, AlertMessage, AlertState } from '../../common/types/alerts'; import { AlertSeverity } from '../../common/enums'; -import { AlertMessage, AlertState } from '../../server/alerts/types'; import { AlertsBadge } from './badge'; import { isInSetupMode } from '../lib/setup_mode'; import { SetupModeContext } from '../components/setup_mode/setup_mode_context'; diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx new file mode 100644 index 0000000000000..5e8e676448218 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { Expression, Props } from '../components/duration/expression'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { CommonAlertParamDetails } from '../../../common/types/alerts'; + +interface ThreadPoolTypes { + [key: string]: unknown; +} + +interface ThreadPoolRejectionAlertDetails { + label: string; + paramDetails: CommonAlertParamDetails; +} + +export function createThreadPoolRejectionsAlertType( + alertType: string, + threadPoolAlertDetails: ThreadPoolRejectionAlertDetails +): AlertTypeModel { + return { + id: alertType, + name: threadPoolAlertDetails.label, + iconClass: 'bell', + alertParamsExpression: (props: Props) => ( + <> + + + + ), + validate: (inputValues: ThreadPoolTypes) => { + const errors: { [key: string]: string[] } = {}; + const value = inputValues.threshold as number; + if (value < 0) { + const errStr = i18n.translate('xpack.monitoring.alerts.validation.lessThanZero', { + defaultMessage: 'This value can not be less than zero', + }); + errors.threshold = [errStr]; + } + + if (!inputValues.duration) { + const errStr = i18n.translate('xpack.monitoring.alerts.validation.duration', { + defaultMessage: 'A valid duration is required.', + }); + errors.duration = [errStr]; + } + + return { errors }; + }, + defaultActionMessage: '{{context.internalFullMessage}}', + requiresAppContext: true, + }; +} diff --git a/x-pack/plugins/monitoring/public/angular/providers/private.js b/x-pack/plugins/monitoring/public/angular/providers/private.js index 3a667037b2919..7709865432fe6 100644 --- a/x-pack/plugins/monitoring/public/angular/providers/private.js +++ b/x-pack/plugins/monitoring/public/angular/providers/private.js @@ -81,9 +81,9 @@ * * @param {[type]} prov [description] */ -import _ from 'lodash'; +import { partial, uniqueId, isObject } from 'lodash'; -const nextId = _.partial(_.uniqueId, 'privateProvider#'); +const nextId = partial(uniqueId, 'privateProvider#'); function name(fn) { return fn.name || fn.toString().split('\n').shift(); @@ -141,7 +141,7 @@ export function PrivateProvider() { const context = {}; let instance = $injector.invoke(prov, context, locals); - if (!_.isObject(instance)) instance = context; + if (!isObject(instance)) instance = context; privPath.pop(); return instance; @@ -155,7 +155,7 @@ export function PrivateProvider() { if ($delegateId != null && $delegateProv != null) { instance = instantiate(prov, { - $decorate: _.partial(get, $delegateId, $delegateProv), + $decorate: partial(get, $delegateId, $delegateProv), }); } else { instance = instantiate(prov); diff --git a/x-pack/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/plugins/monitoring/public/components/chart/chart_target.js index 9a590d803bb19..519964e4d5914 100644 --- a/x-pack/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/plugins/monitoring/public/components/chart/chart_target.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { get, isEqual, filter } from 'lodash'; import $ from 'jquery'; import React from 'react'; import { eventBus } from './event_bus'; @@ -50,12 +50,12 @@ export class ChartTarget extends React.Component { } UNSAFE_componentWillReceiveProps(newProps) { - if (this.plot && !_.isEqual(newProps, this.props)) { + if (this.plot && !isEqual(newProps, this.props)) { const { series, timeRange } = newProps; const xaxisOptions = this.plot.getAxes().xaxis.options; - xaxisOptions.min = _.get(timeRange, 'min'); - xaxisOptions.max = _.get(timeRange, 'max'); + xaxisOptions.min = get(timeRange, 'min'); + xaxisOptions.max = get(timeRange, 'max'); this.plot.setData(this.filterData(series, newProps.seriesToShow)); this.plot.setupGrid(); @@ -73,7 +73,7 @@ export class ChartTarget extends React.Component { } filterData(data, seriesToShow) { - return _(data).filter(this.filterByShow(seriesToShow)).value(); + return filter(data, this.filterByShow(seriesToShow)); } async getOptions() { @@ -128,7 +128,7 @@ export class ChartTarget extends React.Component { this.handleThorPlotHover = (_event, pos, item, originalPlot) => { if (this.plot !== originalPlot) { // the crosshair is set for the original chart already - this.plot.setCrosshair({ x: _.get(pos, 'x') }); + this.plot.setCrosshair({ x: get(pos, 'x') }); } this.props.updateLegend(pos, item); }; diff --git a/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js index eb32ee108e7b3..829994791f769 100644 --- a/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js +++ b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { debounce, keys, has, includes, isFunction, difference, assign } from 'lodash'; import React from 'react'; import { getLastValue } from './get_last_value'; import { TimeseriesContainer } from './timeseries_container'; @@ -17,7 +17,7 @@ export class TimeseriesVisualization extends React.Component { constructor(props) { super(props); - this.debouncedUpdateLegend = _.debounce(this.updateLegend, DEBOUNCE_SLOW_MS); + this.debouncedUpdateLegend = debounce(this.updateLegend, DEBOUNCE_SLOW_MS); this.debouncedUpdateLegend = this.debouncedUpdateLegend.bind(this); this.toggleFilter = this.toggleFilter.bind(this); @@ -26,18 +26,18 @@ export class TimeseriesVisualization extends React.Component { this.state = { values: {}, - seriesToShow: _.keys(values), + seriesToShow: keys(values), ignoreVisibilityUpdates: false, }; } filterLegend(id) { - if (!_.has(this.state.values, id)) { + if (!has(this.state.values, id)) { return []; } - const notAllShown = _.keys(this.state.values).length !== this.state.seriesToShow.length; - const isCurrentlyShown = _.includes(this.state.seriesToShow, id); + const notAllShown = keys(this.state.values).length !== this.state.seriesToShow.length; + const isCurrentlyShown = includes(this.state.seriesToShow, id); const seriesToShow = []; if (notAllShown && isCurrentlyShown) { @@ -59,7 +59,7 @@ export class TimeseriesVisualization extends React.Component { toggleFilter(_event, id) { const seriesToShow = this.filterLegend(id); - if (_.isFunction(this.props.onFilter)) { + if (isFunction(this.props.onFilter)) { this.props.onFilter(seriesToShow); } } @@ -94,7 +94,7 @@ export class TimeseriesVisualization extends React.Component { getValuesByX(this.props.series, pos.x, setValueCallback); } } else { - _.assign(values, this.getLastValues()); + assign(values, this.getLastValues()); } this.setState({ values }); @@ -102,13 +102,13 @@ export class TimeseriesVisualization extends React.Component { UNSAFE_componentWillReceiveProps(props) { const values = this.getLastValues(props); - const currentKeys = _.keys(this.state.values); - const keys = _.keys(values); - const diff = _.difference(keys, currentKeys); + const currentKeys = keys(this.state.values); + const valueKeys = keys(values); + const diff = difference(valueKeys, currentKeys); const nextState = { values: values }; if (diff.length && !this.state.ignoreVisibilityUpdates) { - nextState.seriesToShow = keys; + nextState.seriesToShow = valueKeys; } this.setState(nextState); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 0fe434afa2c88..7e85d62c4bbd6 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -41,6 +41,8 @@ import { ALERT_CLUSTER_HEALTH, ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_NODES_CHANGED, ALERT_ELASTICSEARCH_VERSION_MISMATCH, @@ -162,6 +164,8 @@ const OVERVIEW_PANEL_ALERTS = [ALERT_CLUSTER_HEALTH, ALERT_LICENSE_EXPIRATION]; const NODES_PANEL_ALERTS = [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_NODES_CHANGED, ALERT_ELASTICSEARCH_VERSION_MISMATCH, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 41d3a579db5a2..61188487e2f99 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -27,7 +27,7 @@ import { EuiHealth, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; +import { get } from 'lodash'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { ListingCallOut } from '../../setup_mode/listing_callout'; @@ -58,7 +58,7 @@ const getNodeTooltip = (node) => { return null; }; -const getSortHandler = (type) => (item) => _.get(item, [type, 'summary', 'lastVal']); +const getSortHandler = (type) => (item) => get(item, [type, 'summary', 'lastVal']); const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts) => { const cols = []; @@ -87,7 +87,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler let setupModeStatus = null; if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { - const list = _.get(setupMode, 'data.byUuid', {}); + const list = get(setupMode, 'data.byUuid', {}); const status = list[node.resolver] || {}; const instance = { uuid: node.resolver, @@ -396,7 +396,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear setupMode.data.totalUniqueInstanceCount ) { const finishMigrationAction = - _.get(setupMode.meta, 'liveClusterUuid') === clusterUuid + get(setupMode.meta, 'liveClusterUuid') === clusterUuid ? setupMode.shortcutToFinishMigration : setupMode.openFlyout; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js index 2c66d14a40605..5c8dca54894b4 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { sortBy } from 'lodash'; import React from 'react'; import { Shard } from './shard'; import { i18n } from '@kbn/i18n'; @@ -36,7 +36,7 @@ export class Unassigned extends React.Component { }; render() { - const shards = _.sortBy(this.props.shards, 'shard').map(this.createShard); + const shards = sortBy(this.props.shards, 'shard').map(this.createShard); return ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js index 47739b8fe31e8..a371f3e5ff40c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { some } from 'lodash'; export function hasPrimaryChildren(item) { - return _.some(item.children, { primary: true }); + return some(item.children, { primary: true }); } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js index db9b7bacc3cdf..335c3d29a5b9e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { each, isArray } from 'lodash'; export const _vents = {}; export const vents = { vents: _vents, on: function (id, cb) { - if (!_.isArray(_vents[id])) { + if (!isArray(_vents[id])) { _vents[id] = []; } _vents[id].push(cb); @@ -22,7 +22,7 @@ export const vents = { const args = Array.prototype.slice.call(arguments); const id = args.shift(); if (_vents[id]) { - _.each(_vents[id], function (cb) { + each(_vents[id], function (cb) { cb.apply(null, args); }); } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js index a9808ebc4c6ad..a04e2bcd1786e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { find, reduce, values, sortBy } from 'lodash'; import { decorateShards } from '../lib/decorate_shards'; export function indicesByNodes() { @@ -39,7 +39,7 @@ export function indicesByNodes() { return obj; } - let nodeObj = _.find(obj[index].children, { id: node }); + let nodeObj = find(obj[index].children, { id: node }); if (!nodeObj) { nodeObj = { id: node, @@ -55,7 +55,7 @@ export function indicesByNodes() { return obj; } - const data = _.reduce( + const data = reduce( decorateShards(shards, nodes), function (obj, shard) { obj = createIndex(obj, shard); @@ -64,10 +64,11 @@ export function indicesByNodes() { }, {} ); - - return _(data) - .values() - .sortBy((index) => [!index.unassignedPrimaries, /^\./.test(index.name), index.name]) - .value(); + const dataValues = values(data); + return sortBy(dataValues, (index) => [ + !index.unassignedPrimaries, + /^\./.test(index.name), + index.name, + ]); }; } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js index 353e1c23d4bc1..f8dd5b6cb8e8d 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { find, some, reduce, values, sortBy } from 'lodash'; import { hasPrimaryChildren } from '../lib/has_primary_children'; import { decorateShards } from '../lib/decorate_shards'; @@ -32,7 +32,7 @@ export function nodesByIndices() { if (!obj[node]) { createNode(obj, nodes[node], node); } - let indexObj = _.find(obj[node].children, { id: index }); + let indexObj = find(obj[node].children, { id: index }); if (!indexObj) { indexObj = { id: index, @@ -51,7 +51,7 @@ export function nodesByIndices() { } let data = {}; - if (_.some(shards, isUnassigned)) { + if (some(shards, isUnassigned)) { data.unassigned = { name: 'Unassigned', master: false, @@ -60,19 +60,15 @@ export function nodesByIndices() { }; } - data = _.reduce(decorateShards(shards, nodes), createIndexAddShard, data); - - return _(data) - .values() - .sortBy(function (node) { - return [node.name !== 'Unassigned', !node.master, node.name]; - }) - .map(function (node) { - if (node.name === 'Unassigned') { - node.unassignedPrimaries = node.children.some(hasPrimaryChildren); - } - return node; - }) - .value(); + data = reduce(decorateShards(shards, nodes), createIndexAddShard, data); + const dataValues = values(data); + return sortBy(dataValues, function (node) { + return [node.name !== 'Unassigned', !node.master, node.name]; + }).map(function (node) { + if (node.name === 'Unassigned') { + node.unassignedPrimaries = node.children.some(hasPrimaryChildren); + } + return node; + }); }; } diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index bb9f73b5e9ddb..c3c903dab38e9 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -5,7 +5,6 @@ */ import { CoreStart, HttpSetup, IUiSettingsClient } from 'kibana/public'; -import angular from 'angular'; import { Observable } from 'rxjs'; import { HttpRequestInit } from '../../../../src/core/public'; import { MonitoringStartPluginDependencies } from './types'; diff --git a/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js b/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js index 6aee89a9817d5..cd504374da2e4 100644 --- a/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js +++ b/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js @@ -5,10 +5,10 @@ */ import { set } from '@elastic/safer-lodash-set'; -import _ from 'lodash'; +import { get, each } from 'lodash'; function addOne(obj, key) { - let value = _.get(obj, key); + let value = get(obj, key); set(obj, key, ++value); } @@ -34,8 +34,8 @@ export function calculateShardStats(state) { data[shard.index] = metrics; }; if (state) { - const shards = _.get(state, 'cluster_state.shards'); - _.each(shards, processShards); + const shards = get(state, 'cluster_state.shards'); + each(shards, processShards); } return data; } diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js index 73422219add95..6e05c02ac7338 100644 --- a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js +++ b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { find, first } from 'lodash'; export function getClusterFromClusters(clusters, globalState, unsetGlobalState = false) { const cluster = (() => { - const existingCurrent = _.find(clusters, { cluster_uuid: globalState.cluster_uuid }); + const existingCurrent = find(clusters, { cluster_uuid: globalState.cluster_uuid }); if (existingCurrent) { return existingCurrent; } - const firstCluster = _.first(clusters); + const firstCluster = first(clusters); if (firstCluster && firstCluster.cluster_uuid) { return firstCluster; } diff --git a/x-pack/plugins/monitoring/public/lib/route_init.js b/x-pack/plugins/monitoring/public/lib/route_init.js index eebdfa8692f1a..97ff621ee3164 100644 --- a/x-pack/plugins/monitoring/public/lib/route_init.js +++ b/x-pack/plugins/monitoring/public/lib/route_init.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { isInSetupMode } from './setup_mode'; import { getClusterFromClusters } from './get_cluster_from_clusters'; @@ -13,7 +12,7 @@ export function routeInitProvider(Private, monitoringClusters, globalState, lice const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); function isOnPage(hash) { - return _.includes(window.location.hash, hash); + return window.location.hash.includes(hash); } /* diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 4c50abb40dd3d..a228c540761b8 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -22,11 +22,11 @@ import { UI_SETTINGS } from '../../../../src/plugins/data/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; -import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; -import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; -import { createLegacyAlertTypes } from './alerts/legacy_alert'; -import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; -import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; +import { + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, + ALERT_DETAILS, +} from '../common/constants'; interface MonitoringSetupPluginDependencies { home?: HomePublicPluginSetup; @@ -40,7 +40,7 @@ export class MonitoringPlugin Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public setup( + public async setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { @@ -73,16 +73,7 @@ export class MonitoringPlugin }); } - const { alertTypeRegistry } = plugins.triggersActionsUi; - alertTypeRegistry.register(createCpuUsageAlertType()); - alertTypeRegistry.register(createDiskUsageAlertType()); - alertTypeRegistry.register(createMemoryUsageAlertType()); - alertTypeRegistry.register(createMissingMonitoringDataAlertType()); - - const legacyAlertTypes = createLegacyAlertTypes(); - for (const legacyAlertType of legacyAlertTypes) { - alertTypeRegistry.register(legacyAlertType); - } + await this.registerAlertsAsync(plugins); const app: App = { id, @@ -106,7 +97,6 @@ export class MonitoringPlugin usageCollection: plugins.usageCollection, }; - pluginsStart.kibanaLegacy.loadFontAwesome(); this.setInitialTimefilter(deps); const monitoringApp = new AngularApp(deps); @@ -154,4 +144,41 @@ export class MonitoringPlugin ['showCgroupMetricsLogstash', monitoring.ui.container.logstash.enabled], ]; } + + private registerAlertsAsync = async (plugins: MonitoringSetupPluginDependencies) => { + const { createCpuUsageAlertType } = await import('./alerts/cpu_usage_alert'); + const { createMissingMonitoringDataAlertType } = await import( + './alerts/missing_monitoring_data_alert' + ); + const { createLegacyAlertTypes } = await import('./alerts/legacy_alert'); + const { createDiskUsageAlertType } = await import('./alerts/disk_usage_alert'); + const { createThreadPoolRejectionsAlertType } = await import( + './alerts/thread_pool_rejections_alert' + ); + const { createMemoryUsageAlertType } = await import('./alerts/memory_usage_alert'); + + const { + triggersActionsUi: { alertTypeRegistry }, + } = plugins; + alertTypeRegistry.register(createCpuUsageAlertType()); + alertTypeRegistry.register(createDiskUsageAlertType()); + alertTypeRegistry.register(createMemoryUsageAlertType()); + alertTypeRegistry.register(createMissingMonitoringDataAlertType()); + alertTypeRegistry.register( + createThreadPoolRejectionsAlertType( + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS] + ) + ); + alertTypeRegistry.register( + createThreadPoolRejectionsAlertType( + ALERT_THREAD_POOL_WRITE_REJECTIONS, + ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS] + ) + ); + const legacyAlertTypes = createLegacyAlertTypes(); + for (const legacyAlertType of legacyAlertTypes) { + alertTypeRegistry.register(legacyAlertType); + } + }; } diff --git a/x-pack/plugins/monitoring/public/services/features.js b/x-pack/plugins/monitoring/public/services/features.js index f98af10f8dfb4..5e29353e497d1 100644 --- a/x-pack/plugins/monitoring/public/services/features.js +++ b/x-pack/plugins/monitoring/public/services/features.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { has, isUndefined } from 'lodash'; export function featuresProvider($window) { function getData() { @@ -28,11 +28,11 @@ export function featuresProvider($window) { function isEnabled(featureName, defaultSetting) { const monitoringDataObj = getData(); - if (_.has(monitoringDataObj, featureName)) { + if (has(monitoringDataObj, featureName)) { return monitoringDataObj[featureName]; } - if (_.isUndefined(defaultSetting)) { + if (isUndefined(defaultSetting)) { return false; } diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js index 0715f4dc9e0b6..91ef4c32f3b98 100644 --- a/x-pack/plugins/monitoring/public/services/title.js +++ b/x-pack/plugins/monitoring/public/services/title.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Legacy } from '../legacy_shims'; export function titleProvider($rootScope) { return function changeTitle(cluster, suffix) { - let clusterName = _.get(cluster, 'cluster_name'); + let clusterName = get(cluster, 'cluster_name'); clusterName = clusterName ? `- ${clusterName}` : ''; suffix = suffix ? `- ${suffix}` : ''; $rootScope.$applyAsync(() => { diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js index 8021ae7e5f63c..7e78170d1117f 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -20,6 +20,8 @@ import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_MEMORY_USAGE, @@ -76,6 +78,8 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', { alertTypeIds: [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_MISSING_MONITORING_DATA, ], diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index 5164e93c266ca..586261eecb250 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -21,6 +21,8 @@ import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_MEMORY_USAGE, @@ -60,6 +62,8 @@ uiRoutes.when('/elasticsearch/nodes/:node', { alertTypeIds: [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_MISSING_MONITORING_DATA, ], diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index e69d572f9560b..3ec9c6235867b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -19,6 +19,8 @@ import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_MEMORY_USAGE, @@ -93,6 +95,8 @@ uiRoutes.when('/elasticsearch/nodes', { alertTypeIds: [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_MISSING_MONITORING_DATA, ], diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_common.ts b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts similarity index 97% rename from x-pack/plugins/monitoring/server/alerts/alerts_common.ts rename to x-pack/plugins/monitoring/server/alerts/alert_helpers.ts index 41c8bba17df0a..984746e59f06b 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_common.ts +++ b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { AlertMessageDocLinkToken } from './types'; +import { AlertMessageDocLinkToken } from '../../common/types/alerts'; import { AlertMessageTokenType } from '../../common/enums'; export class AlertingDefaults { diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts index f486061109b39..cc0423051f2aa 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -60,9 +60,4 @@ describe('AlertsFactory', () => { expect(alert).not.toBeNull(); expect(alert?.type).toBe(ALERT_CPU_USAGE); }); - - it('should get all', () => { - const alerts = AlertsFactory.getAll(); - expect(alerts.length).toBe(10); - }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts index 22c41c9c60038..efd3d7d5e3b30 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -8,6 +8,8 @@ import { CpuUsageAlert, MissingMonitoringDataAlert, DiskUsageAlert, + ThreadPoolSearchRejectionsAlert, + ThreadPoolWriteRejectionsAlert, MemoryUsageAlert, NodesChangedAlert, ClusterHealthAlert, @@ -23,6 +25,8 @@ import { ALERT_CPU_USAGE, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_NODES_CHANGED, ALERT_LOGSTASH_VERSION_MISMATCH, @@ -31,12 +35,14 @@ import { } from '../../common/constants'; import { AlertsClient } from '../../../alerts/server'; -export const BY_TYPE = { +const BY_TYPE = { [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert, [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert, [ALERT_CPU_USAGE]: CpuUsageAlert, [ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert, [ALERT_DISK_USAGE]: DiskUsageAlert, + [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: ThreadPoolSearchRejectionsAlert, + [ALERT_THREAD_POOL_WRITE_REJECTIONS]: ThreadPoolWriteRejectionsAlert, [ALERT_MEMORY_USAGE]: MemoryUsageAlert, [ALERT_NODES_CHANGED]: NodesChangedAlert, [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert, diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index c92291cf72093..48b783a450807 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -28,14 +28,16 @@ import { AlertData, AlertInstanceState, AlertEnableAction, -} from './types'; + CommonAlertFilter, + CommonAlertParams, + CommonBaseAlert, +} from '../../common/types/alerts'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { MonitoringConfig } from '../config'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertFilter, CommonAlertParams, CommonBaseAlert } from '../../common/types'; import { MonitoringLicenseService } from '../types'; import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; @@ -269,18 +271,18 @@ export class BaseAlert { } protected async fetchData( - params: CommonAlertParams, + params: CommonAlertParams | unknown, callCluster: any, clusters: AlertCluster[], uiSettings: IUiSettingsClient, availableCcs: string[] - ): Promise { + ): Promise> { // Child should implement throw new Error('Child classes must implement `fetchData`'); } protected async processData( - data: AlertData[], + data: Array, clusters: AlertCluster[], services: AlertServices, logger: Logger, @@ -365,15 +367,18 @@ export class BaseAlert { }; } - protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + protected getUiMessage( + alertState: AlertState | unknown, + item: AlertData | unknown + ): AlertMessage { throw new Error('Child classes must implement `getUiMessage`'); } protected executeActions( instance: AlertInstance, - instanceState: AlertInstanceState, - item: AlertData, - cluster: AlertCluster + instanceState: AlertInstanceState | unknown, + item: AlertData | unknown, + cluster?: AlertCluster | unknown ) { throw new Error('Child classes must implement `executeActions`'); } diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 427dd2f86de00..1d3d36413ebc2 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -14,15 +14,15 @@ import { AlertMessageLinkToken, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH, LEGACY_ALERT_DETAILS } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; -import { CommonAlertParams } from '../../common/types'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { defaultMessage: 'Allocate missing primary and replica shards', @@ -39,9 +39,7 @@ const WATCH_NAME = 'elasticsearch_cluster_status'; export class ClusterHealthAlert extends BaseAlert { public type = ALERT_CLUSTER_HEALTH; - public label = i18n.translate('xpack.monitoring.alerts.clusterHealth.label', { - defaultMessage: 'Cluster health', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 09133dadca162..55931e2996cbf 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -16,55 +16,36 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertNodeUuidFilter, + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; -import { INDEX_PATTERN_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../common/constants'; +import { + INDEX_PATTERN_ELASTICSEARCH, + ALERT_CPU_USAGE, + ALERT_DETAILS, +} from '../../common/constants'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; import { parseDuration } from '../../../alerts/common/parse_duration'; -import { - CommonAlertFilter, - CommonAlertNodeUuidFilter, - CommonAlertParams, - CommonAlertParamDetail, -} from '../../common/types'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; -const DEFAULT_THRESHOLD = 85; -const DEFAULT_DURATION = '5m'; - interface CpuUsageParams { threshold: number; duration: string; } export class CpuUsageAlert extends BaseAlert { - public static paramDetails = { - threshold: { - label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label', { - defaultMessage: `Notify when CPU is over`, - }), - type: AlertParamType.Percentage, - } as CommonAlertParamDetail, - duration: { - label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label', { - defaultMessage: `Look at the average over`, - }), - type: AlertParamType.Duration, - } as CommonAlertParamDetail, - }; - public type = ALERT_CPU_USAGE; - public label = i18n.translate('xpack.monitoring.alerts.cpuUsage.label', { - defaultMessage: 'CPU Usage', - }); + public label = ALERT_DETAILS[ALERT_CPU_USAGE].label; protected defaultParams: CpuUsageParams = { - threshold: DEFAULT_THRESHOLD, - duration: DEFAULT_DURATION, + threshold: 85, + duration: '5m', }; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index 34c640de79625..e54e736724357 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -15,43 +15,25 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; -import { INDEX_PATTERN_ELASTICSEARCH, ALERT_DISK_USAGE } from '../../common/constants'; +import { + INDEX_PATTERN_ELASTICSEARCH, + ALERT_DISK_USAGE, + ALERT_DETAILS, +} from '../../common/constants'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; -import { CommonAlertFilter, CommonAlertParams, CommonAlertParamDetail } from '../../common/types'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; -interface ParamDetails { - [key: string]: CommonAlertParamDetail; -} - export class DiskUsageAlert extends BaseAlert { - public static readonly PARAM_DETAILS: ParamDetails = { - threshold: { - label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label', { - defaultMessage: `Notify when disk capacity is over`, - }), - type: AlertParamType.Percentage, - }, - duration: { - label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.duration.label', { - defaultMessage: `Look at the average over`, - }), - type: AlertParamType.Duration, - }, - }; - public static paramDetails = DiskUsageAlert.PARAM_DETAILS; - public static readonly TYPE = ALERT_DISK_USAGE; - public static readonly LABEL = i18n.translate('xpack.monitoring.alerts.diskUsage.label', { - defaultMessage: 'Disk Usage', - }); - public type = DiskUsageAlert.TYPE; - public label = DiskUsageAlert.LABEL; + public type = ALERT_DISK_USAGE; + public label = ALERT_DETAILS[ALERT_DISK_USAGE].label; protected defaultParams = { threshold: 80, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index f26b21f0c64c5..6412dcfde54bd 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -13,22 +13,24 @@ import { AlertMessage, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { + INDEX_ALERTS, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, +} from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'elasticsearch_version_mismatch'; export class ElasticsearchVersionMismatchAlert extends BaseAlert { public type = ALERT_ELASTICSEARCH_VERSION_MISMATCH; - public label = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', { - defaultMessage: 'Elasticsearch version mismatch', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts index 48254f2dec326..5fa718dfb34cd 100644 --- a/x-pack/plugins/monitoring/server/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -8,6 +8,8 @@ export { BaseAlert } from './base_alert'; export { CpuUsageAlert } from './cpu_usage_alert'; export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; export { DiskUsageAlert } from './disk_usage_alert'; +export { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert'; +export { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert'; export { MemoryUsageAlert } from './memory_usage_alert'; export { ClusterHealthAlert } from './cluster_health_alert'; export { LicenseExpirationAlert } from './license_expiration_alert'; @@ -15,4 +17,4 @@ export { NodesChangedAlert } from './nodes_changed_alert'; export { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; export { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; export { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; -export { AlertsFactory, BY_TYPE } from './alerts_factory'; +export { AlertsFactory } from './alerts_factory'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 316f305603964..851a401635792 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -13,22 +13,24 @@ import { AlertMessage, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { + INDEX_ALERTS, + ALERT_KIBANA_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, +} from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'kibana_version_mismatch'; export class KibanaVersionMismatchAlert extends BaseAlert { public type = ALERT_KIBANA_VERSION_MISMATCH; - public label = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', { - defaultMessage: 'Kibana version mismatch', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index f1412ff0fc91a..e0396ee6673e8 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -16,27 +16,26 @@ import { AlertMessageLinkToken, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; import { INDEX_ALERTS, ALERT_LICENSE_EXPIRATION, FORMAT_DURATION_TEMPLATE_SHORT, + LEGACY_ALERT_DETAILS, } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'xpack_license_expiration'; export class LicenseExpirationAlert extends BaseAlert { public type = ALERT_LICENSE_EXPIRATION; - public label = i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', { - defaultMessage: 'License expiration', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label; public isLegacy = true; protected actionVariables = [ { diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 37515e32e591a..7f5c0ea40e36a 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -13,22 +13,24 @@ import { AlertMessage, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { + INDEX_ALERTS, + ALERT_LOGSTASH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, +} from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'logstash_version_mismatch'; export class LogstashVersionMismatchAlert extends BaseAlert { public type = ALERT_LOGSTASH_VERSION_MISMATCH; - public label = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', { - defaultMessage: 'Logstash version mismatch', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts index 8dc707afab1e1..c37176764c020 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts @@ -15,44 +15,26 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; -import { INDEX_PATTERN_ELASTICSEARCH, ALERT_MEMORY_USAGE } from '../../common/constants'; +import { + INDEX_PATTERN_ELASTICSEARCH, + ALERT_MEMORY_USAGE, + ALERT_DETAILS, +} from '../../common/constants'; import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; -import { CommonAlertFilter, CommonAlertParams, CommonAlertParamDetail } from '../../common/types'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerts/common/parse_duration'; -interface ParamDetails { - [key: string]: CommonAlertParamDetail; -} - export class MemoryUsageAlert extends BaseAlert { - public static readonly PARAM_DETAILS: ParamDetails = { - threshold: { - label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label', { - defaultMessage: `Notify when memory usage is over`, - }), - type: AlertParamType.Percentage, - }, - duration: { - label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label', { - defaultMessage: `Look at the average over`, - }), - type: AlertParamType.Duration, - }, - }; - public static paramDetails = MemoryUsageAlert.PARAM_DETAILS; - public static readonly TYPE = ALERT_MEMORY_USAGE; - public static readonly LABEL = i18n.translate('xpack.monitoring.alerts.memoryUsage.label', { - defaultMessage: 'Memory Usage (JVM)', - }); - public type = MemoryUsageAlert.TYPE; - public label = MemoryUsageAlert.LABEL; + public type = ALERT_MEMORY_USAGE; + public label = ALERT_DETAILS[ALERT_MEMORY_USAGE].label; protected defaultParams = { threshold: 85, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index 5b4542a4439ca..456ad92855f65 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -16,24 +16,22 @@ import { AlertMissingData, AlertMessageTimeToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertParams, + CommonAlertStackProductFilter, + CommonAlertNodeUuidFilter, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; import { INDEX_PATTERN, ALERT_MISSING_MONITORING_DATA, INDEX_PATTERN_ELASTICSEARCH, + ALERT_DETAILS, } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; import { parseDuration } from '../../../alerts/common/parse_duration'; -import { - CommonAlertFilter, - CommonAlertParams, - CommonAlertParamDetail, - CommonAlertStackProductFilter, - CommonAlertNodeUuidFilter, -} from '../../common/types'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; import { getTypeLabelForStackProduct } from '../lib/alerts/get_type_label_for_stack_product'; @@ -41,7 +39,7 @@ import { getListingLinkForStackProduct } from '../lib/alerts/get_listing_link_fo import { getStackProductLabel } from '../lib/alerts/get_stack_product_label'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', { defaultMessage: 'resolved', @@ -62,27 +60,10 @@ interface MissingDataParams { } export class MissingMonitoringDataAlert extends BaseAlert { - public static paramDetails = { - duration: { - label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', { - defaultMessage: `Notify if monitoring data is missing for the last`, - }), - type: AlertParamType.Duration, - } as CommonAlertParamDetail, - limit: { - label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', { - defaultMessage: `looking back`, - }), - type: AlertParamType.Duration, - } as CommonAlertParamDetail, - }; - public defaultThrottle: string = '6h'; public type = ALERT_MISSING_MONITORING_DATA; - public label = i18n.translate('xpack.monitoring.alerts.missingData.label', { - defaultMessage: 'Missing monitoring data', - }); + public label = ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label; protected defaultParams: MissingDataParams = { duration: DEFAULT_DURATION, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index e03e6ea53ab4e..7b54ef629cba6 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -14,22 +14,20 @@ import { AlertInstanceState, LegacyAlert, LegacyAlertNodesChangedList, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_NODES_CHANGED } from '../../common/constants'; +import { INDEX_ALERTS, ALERT_NODES_CHANGED, LEGACY_ALERT_DETAILS } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'elasticsearch_nodes'; export class NodesChangedAlert extends BaseAlert { public type = ALERT_NODES_CHANGED; - public label = i18n.translate('xpack.monitoring.alerts.nodesChanged.label', { - defaultMessage: 'Nodes changed', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts new file mode 100644 index 0000000000000..4905ae73b0545 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient, Logger } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertMessage, + AlertThreadPoolRejectionsState, + AlertMessageTimeToken, + AlertMessageLinkToken, + CommonAlertFilter, + ThreadPoolRejectionsAlertParams, +} from '../../common/types/alerts'; +import { AlertInstance, AlertServices } from '../../../alerts/server'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; +import { Alert, RawAlertInstance } from '../../../alerts/common'; +import { AlertingDefaults, createLink } from './alert_helpers'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; + +type ActionVariables = Array<{ name: string; description: string }>; + +export class ThreadPoolRejectionsAlertBase extends BaseAlert { + protected static createActionVariables(type: string) { + return [ + { + name: 'count', + description: i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.actionVariables.count', + { + defaultMessage: 'The number of nodes reporting high thread pool {type} rejections.', + values: { type }, + } + ), + }, + ...Object.values(AlertingDefaults.ALERT_TYPE.context), + ]; + } + + protected defaultParams: ThreadPoolRejectionsAlertParams = { + threshold: 300, + duration: '5m', + }; + + constructor( + rawAlert: Alert | undefined = undefined, + public readonly type: string, + public readonly threadPoolType: string, + public readonly label: string, + public readonly actionVariables: ActionVariables + ) { + super(rawAlert); + } + + protected async fetchData( + params: ThreadPoolRejectionsAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + + const { threshold, duration } = params; + + const stats = await fetchThreadPoolRejectionStats( + callCluster, + clusters, + esIndexPattern, + this.config.ui.max_bucket_size, + this.threadPoolType, + duration + ); + + return stats.map((stat) => { + const { clusterUuid, nodeId, rejectionCount, ccs } = stat; + + return { + instanceKey: `${clusterUuid}:${nodeId}`, + shouldFire: rejectionCount > threshold, + rejectionCount, + severity: AlertSeverity.Danger, + meta: stat, + clusterUuid, + ccs, + }; + }); + } + + protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) { + const alertInstanceStates = alertInstance.state + ?.alertStates as AlertThreadPoolRejectionsState[]; + const nodeUuid = filters?.find((filter) => filter.nodeUuid)?.nodeUuid; + + if (!alertInstanceStates?.length || !nodeUuid) { + return true; + } + + const nodeAlerts = alertInstanceStates.filter(({ nodeId }) => nodeId === nodeUuid); + return Boolean(nodeAlerts.length); + } + + protected getUiMessage( + alertState: AlertThreadPoolRejectionsState, + rejectionCount: number + ): AlertMessage { + const { nodeName, nodeId } = alertState; + return { + text: i18n.translate('xpack.monitoring.alerts.threadPoolRejections.ui.firingMessage', { + defaultMessage: `Node #start_link{nodeName}#end_link is reporting {rejectionCount} {type} rejections at #absolute`, + values: { + nodeName, + type: this.threadPoolType, + rejectionCount, + }, + }), + nextSteps: [ + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.monitorThisNode', + { + defaultMessage: `#start_linkMonitor this node#end_link`, + } + ), + `elasticsearch/nodes/${nodeId}/advanced`, + AlertMessageTokenType.Link + ), + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.optimizeQueries', + { + defaultMessage: '#start_linkOptimize complex queries#end_link', + } + ), + `{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries` + ), + createLink( + i18n.translate('xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.addMoreNodes', { + defaultMessage: '#start_linkAdd more nodes#end_link', + }), + `{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html` + ), + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.resizeYourDeployment', + { + defaultMessage: '#start_linkResize your deployment (ECE)#end_link', + } + ), + `{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html` + ), + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.threadPoolSettings', + { + defaultMessage: '#start_linkThread pool settings#end_link', + } + ), + `{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html` + ), + ], + tokens: [ + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.triggeredMS, + } as AlertMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: `elasticsearch/nodes/${nodeId}`, + } as AlertMessageLinkToken, + ], + }; + } + + protected executeActions( + instance: AlertInstance, + alertStates: AlertThreadPoolRejectionsState[], + cluster: AlertCluster + ) { + const type = this.threadPoolType; + const count = alertStates.length; + const { clusterName: clusterKnownName, clusterUuid } = cluster; + const clusterName = clusterKnownName || clusterUuid; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.shortAction', + { + defaultMessage: 'Verify thread pool {type} rejections across affected nodes.', + values: { + type, + }, + } + ); + + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.fullAction', + { + defaultMessage: 'View nodes', + } + ); + + const ccs = alertStates.find((state) => state.ccs)?.ccs; + const globalStateLink = this.createGlobalStateLink('elasticsearch/nodes', clusterUuid, ccs); + + const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.firing.internalShortMessage', + { + defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count, + clusterName, + shortActionText, + type, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.firing.internalFullMessage', + { + defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + values: { + count, + clusterName, + action, + type, + }, + } + ); + + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage, + threadPoolType: type, + state: AlertingDefaults.ALERT_STATE.firing, + count, + clusterName, + action, + actionPlain: shortActionText, + }); + } + + protected async processData( + data: AlertData[], + clusters: AlertCluster[], + services: AlertServices, + logger: Logger, + state: { lastChecked?: number } + ) { + const currentUTC = +new Date(); + for (const cluster of clusters) { + const nodes = data.filter((node) => node.clusterUuid === cluster.clusterUuid); + if (!nodes.length) { + continue; + } + + const firingNodeUuids = nodes.filter((node) => node.shouldFire); + + if (!firingNodeUuids.length) { + continue; + } + + const instanceSuffix = firingNodeUuids.map((node) => node.meta.nodeId); + + const instancePrefix = `${this.type}:${cluster.clusterUuid}:`; + const alertInstanceId = `${instancePrefix}:${instanceSuffix}`; + const alertInstance = services.alertInstanceFactory(alertInstanceId); + const newAlertStates: AlertThreadPoolRejectionsState[] = []; + + for (const node of nodes) { + if (!node.shouldFire) { + continue; + } + const stat = node.meta as AlertThreadPoolRejectionsState; + const nodeState = this.getDefaultAlertState( + cluster, + node + ) as AlertThreadPoolRejectionsState; + const { nodeId, nodeName, rejectionCount } = stat; + nodeState.nodeId = nodeId; + nodeState.nodeName = nodeName; + nodeState.ui.triggeredMS = currentUTC; + nodeState.ui.isFiring = true; + nodeState.ui.severity = node.severity; + nodeState.ui.message = this.getUiMessage(nodeState, rejectionCount); + newAlertStates.push(nodeState); + } + + alertInstance.replaceState({ alertStates: newAlertStates }); + if (newAlertStates.length) { + this.executeActions(alertInstance, newAlertStates, cluster); + } + } + + state.lastChecked = currentUTC; + return state; + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts new file mode 100644 index 0000000000000..10df95c05ba3f --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; +import { ALERT_THREAD_POOL_SEARCH_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerts/common'; + +export class ThreadPoolSearchRejectionsAlert extends ThreadPoolRejectionsAlertBase { + private static TYPE = ALERT_THREAD_POOL_SEARCH_REJECTIONS; + private static THREAD_POOL_TYPE = 'search'; + private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS].label; + constructor(rawAlert?: Alert) { + super( + rawAlert, + ThreadPoolSearchRejectionsAlert.TYPE, + ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE, + ThreadPoolSearchRejectionsAlert.LABEL, + ThreadPoolRejectionsAlertBase.createActionVariables( + ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts new file mode 100644 index 0000000000000..d415515315b37 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; +import { ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerts/common'; + +export class ThreadPoolWriteRejectionsAlert extends ThreadPoolRejectionsAlertBase { + private static TYPE = ALERT_THREAD_POOL_WRITE_REJECTIONS; + private static THREAD_POOL_TYPE = 'write'; + private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS].label; + constructor(rawAlert?: Alert) { + super( + rawAlert, + ThreadPoolWriteRejectionsAlert.TYPE, + ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE, + ThreadPoolWriteRejectionsAlert.LABEL, + ThreadPoolRejectionsAlertBase.createActionVariables( + ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index d474338bce922..368a909279b8c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertCluster } from '../../alerts/types'; +import { AlertCluster } from '../../../common/types/alerts'; interface RangeFilter { [field: string]: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index ecd324c083a8c..b38a32164223e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import moment from 'moment'; import { NORMALIZED_DERIVATIVE_UNIT } from '../../../common/constants'; -import { AlertCluster, AlertCpuUsageNodeStats } from '../../alerts/types'; +import { AlertCluster, AlertCpuUsageNodeStats } from '../../../common/types/alerts'; interface NodeBucketESResponse { key: string; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 6201204ebebe0..f00c42d708b16 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { AlertCluster, AlertDiskUsageNodeStats } from '../../alerts/types'; +import { AlertCluster, AlertDiskUsageNodeStats } from '../../../common/types/alerts'; export async function fetchDiskUsageNodeStats( callCluster: any, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts index fe01a1b921c2e..fbf7608a737ba 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../alerts/types'; +import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../../common/types/alerts'; export async function fetchLegacyAlerts( callCluster: any, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index c6843c3ed5f12..9a68b3afc7758 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { AlertCluster, AlertMemoryUsageNodeStats } from '../../alerts/types'; +import { AlertCluster, AlertMemoryUsageNodeStats } from '../../../common/types/alerts'; export async function fetchMemoryUsageNodeStats( callCluster: any, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index 91fc05137a8c1..49307764e9f01 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertCluster, AlertMissingData } from '../../alerts/types'; +import { AlertCluster, AlertMissingData } from '../../../common/types/alerts'; import { KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index 824eeab7245b4..c31ab91866b1d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -5,7 +5,7 @@ */ import { fetchStatus } from './fetch_status'; -import { AlertUiState, AlertState } from '../../alerts/types'; +import { AlertUiState, AlertState } from '../../../common/types/alerts'; import { AlertSeverity } from '../../../common/enums'; import { ALERT_CPU_USAGE, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index ed49f42e4908c..ed860ee21344d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment'; -import { AlertInstanceState } from '../../alerts/types'; +import { AlertInstanceState } from '../../../common/types/alerts'; import { AlertsClient } from '../../../../alerts/server'; import { AlertsFactory } from '../../alerts'; -import { CommonAlertStatus, CommonAlertState, CommonAlertFilter } from '../../../common/types'; +import { + CommonAlertStatus, + CommonAlertState, + CommonAlertFilter, +} from '../../../common/types/alerts'; import { ALERTS } from '../../../common/constants'; import { MonitoringLicenseService } from '../../types'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts new file mode 100644 index 0000000000000..664ceb1d9411b --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { AlertCluster, AlertThreadPoolRejectionsStats } from '../../../common/types/alerts'; + +const invalidNumberValue = (value: number) => { + return isNaN(value) || value === undefined || value === null; +}; + +const getTopHits = (threadType: string, order: string) => ({ + top_hits: { + sort: [ + { + timestamp: { + order, + unmapped_type: 'long', + }, + }, + ], + _source: { + includes: [`node_stats.thread_pool.${threadType}.rejected`, 'source_node.name'], + }, + size: 1, + }, +}); + +export async function fetchThreadPoolRejectionStats( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number, + threadType: string, + duration: string +): Promise { + const clustersIds = clusters.map((cluster) => cluster.clusterUuid); + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clustersIds, + }, + }, + { + term: { + type: 'node_stats', + }, + }, + { + range: { + timestamp: { + gte: `now-${duration}`, + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size, + }, + aggs: { + nodes: { + terms: { + field: 'source_node.uuid', + size, + }, + aggs: { + most_recent: { + ...getTopHits(threadType, 'desc'), + }, + least_recent: { + ...getTopHits(threadType, 'asc'), + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const stats: AlertThreadPoolRejectionsStats[] = []; + const { buckets: clusterBuckets = [] } = response.aggregations.clusters; + + if (!clusterBuckets.length) { + return stats; + } + + for (const clusterBucket of clusterBuckets) { + for (const node of clusterBucket.nodes.buckets) { + const mostRecentDoc = get(node, 'most_recent.hits.hits[0]'); + mostRecentDoc.timestamp = mostRecentDoc.sort[0]; + + const leastRecentDoc = get(node, 'least_recent.hits.hits[0]'); + leastRecentDoc.timestamp = leastRecentDoc.sort[0]; + + if (!mostRecentDoc || mostRecentDoc.timestamp === leastRecentDoc.timestamp) { + continue; + } + + const rejectedPath = `_source.node_stats.thread_pool.${threadType}.rejected`; + const newRejectionCount = Number(get(mostRecentDoc, rejectedPath)); + const oldRejectionCount = Number(get(leastRecentDoc, rejectedPath)); + + if (invalidNumberValue(newRejectionCount) || invalidNumberValue(oldRejectionCount)) { + continue; + } + + const rejectionCount = + oldRejectionCount > newRejectionCount + ? newRejectionCount + : newRejectionCount - oldRejectionCount; + const indexName = mostRecentDoc._index; + const nodeName = get(mostRecentDoc, '_source.source_node.name') || node.key; + const nodeStat = { + rejectionCount, + type: threadType, + clusterUuid: clusterBucket.key, + nodeId: node.key, + nodeName, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + stats.push(nodeStat); + } + } + return stats; +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index d97bc34c2adb0..29a27ac3d05e7 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { handleError } from '../../../../lib/errors'; import { RouteDependencies } from '../../../../types'; import { fetchStatus } from '../../../../lib/alerts/fetch_status'; -import { CommonAlertFilter } from '../../../../../common/types'; +import { CommonAlertFilter } from '../../../../../common/types/alerts'; export function alertStatusRoute(server: any, npRoute: RouteDependencies) { npRoute.router.post( From f5b1faea4703b8263bdb3a5ec025564b0af5e422 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 30 Oct 2020 15:52:30 +0100 Subject: [PATCH 73/73] [Chrome] Extension to append an element to the last breadcrumb (#82015) --- ...omestart.getbreadcrumbsappendextension_.md | 17 ++++ .../kibana-plugin-core-public.chromestart.md | 2 + ...romestart.setbreadcrumbsappendextension.md | 24 +++++ src/core/public/chrome/chrome_service.mock.ts | 3 + src/core/public/chrome/chrome_service.test.ts | 73 +++++++++----- src/core/public/chrome/chrome_service.tsx | 30 ++++++ .../header/__snapshots__/header.test.tsx.snap | 98 +++++++++++++++++++ .../public/chrome/ui/header/header.test.tsx | 1 + src/core/public/chrome/ui/header/header.tsx | 4 +- .../ui/header/header_breadcrumbs.test.tsx | 32 +++++- .../chrome/ui/header/header_breadcrumbs.tsx | 17 +++- .../ui/header/header_extension.test.tsx | 8 ++ .../chrome/ui/header/header_extension.tsx | 8 +- src/core/public/public.api.md | 3 + 14 files changed, 288 insertions(+), 32 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md new file mode 100644 index 0000000000000..dfe25c5c9e42d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getBreadcrumbsAppendExtension$](./kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md) + +## ChromeStart.getBreadcrumbsAppendExtension$() method + +Get an observable of the current extension appended to breadcrumbs + +Signature: + +```typescript +getBreadcrumbsAppendExtension$(): Observable; +``` +Returns: + +`Observable` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index 2594848ef0847..663b326193de5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -55,6 +55,7 @@ core.chrome.setHelpExtension(elem => { | [getBadge$()](./kibana-plugin-core-public.chromestart.getbadge_.md) | Get an observable of the current badge | | [getBrand$()](./kibana-plugin-core-public.chromestart.getbrand_.md) | Get an observable of the current brand information. | | [getBreadcrumbs$()](./kibana-plugin-core-public.chromestart.getbreadcrumbs_.md) | Get an observable of the current list of breadcrumbs | +| [getBreadcrumbsAppendExtension$()](./kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md) | Get an observable of the current extension appended to breadcrumbs | | [getCustomNavLink$()](./kibana-plugin-core-public.chromestart.getcustomnavlink_.md) | Get an observable of the current custom nav link | | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | @@ -64,6 +65,7 @@ core.chrome.setHelpExtension(elem => { | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | | [setBrand(brand)](./kibana-plugin-core-public.chromestart.setbrand.md) | Set the brand configuration. | | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | +| [setBreadcrumbsAppendExtension(breadcrumbsAppendExtension)](./kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md) | Mount an element next to the last breadcrumb | | [setCustomNavLink(newCustomNavLink)](./kibana-plugin-core-public.chromestart.setcustomnavlink.md) | Override the current set of custom nav link | | [setHelpExtension(helpExtension)](./kibana-plugin-core-public.chromestart.sethelpextension.md) | Override the current set of custom help content | | [setHelpSupportUrl(url)](./kibana-plugin-core-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md new file mode 100644 index 0000000000000..02adb9b4d325d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setBreadcrumbsAppendExtension](./kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md) + +## ChromeStart.setBreadcrumbsAppendExtension() method + +Mount an element next to the last breadcrumb + +Signature: + +```typescript +setBreadcrumbsAppendExtension(breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| breadcrumbsAppendExtension | ChromeBreadcrumbsAppendExtension | | + +Returns: + +`void` + diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index cbcd23615d34c..6df8d57c8c574 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -63,6 +63,8 @@ const createStartContractMock = () => { setBadge: jest.fn(), getBreadcrumbs$: jest.fn(), setBreadcrumbs: jest.fn(), + getBreadcrumbsAppendExtension$: jest.fn(), + setBreadcrumbsAppendExtension: jest.fn(), getHelpExtension$: jest.fn(), setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), @@ -76,6 +78,7 @@ const createStartContractMock = () => { startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); + startContract.getBreadcrumbsAppendExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getCustomNavLink$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 0150554a60906..3783f3bd9b65e 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -363,6 +363,25 @@ describe('start', () => { }); }); + describe('breadcrumbsAppendExtension$', () => { + it('updates the breadcrumbsAppendExtension$', async () => { + const { chrome, service } = await start(); + const promise = chrome.getBreadcrumbsAppendExtension$().pipe(toArray()).toPromise(); + + chrome.setBreadcrumbsAppendExtension({ content: (element) => () => {} }); + service.stop(); + + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + undefined, + Object { + "content": [Function], + }, + ] + `); + }); + }); + describe('custom nav link', () => { it('updates/emits the current custom nav link', async () => { const { chrome, service } = await start(); @@ -429,33 +448,33 @@ describe('start', () => { expect(docTitleResetSpy).toBeCalledTimes(1); await expect(promises).resolves.toMatchInlineSnapshot(` - Array [ - Array [ - undefined, - Object { - "appName": "App name", - }, - undefined, - ], - Array [ - Array [], - Array [ - Object { - "text": "App breadcrumb", - }, - ], - Array [], - ], - Array [ - undefined, - Object { - "text": "App badge", - "tooltip": "App tooltip", - }, - undefined, - ], - ] - `); + Array [ + Array [ + undefined, + Object { + "appName": "App name", + }, + undefined, + ], + Array [ + Array [], + Array [ + Object { + "text": "App breadcrumb", + }, + ], + Array [], + ], + Array [ + undefined, + Object { + "text": "App badge", + "tooltip": "App tooltip", + }, + undefined, + ], + ] + `); }); }); }); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index b01f120b81305..b39c83498859c 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -24,6 +24,7 @@ import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } import { flatMap, map, takeUntil } from 'rxjs/operators'; import { parse } from 'url'; import { EuiLink } from '@elastic/eui'; +import { MountPoint } from '../types'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; @@ -58,6 +59,11 @@ export interface ChromeBrand { /** @public */ export type ChromeBreadcrumb = EuiBreadcrumb; +/** @public */ +export interface ChromeBreadcrumbsAppendExtension { + content: MountPoint; +} + /** @public */ export interface ChromeHelpExtension { /** @@ -146,6 +152,9 @@ export class ChromeService { const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); + const breadcrumbsAppendExtension$ = new BehaviorSubject< + ChromeBreadcrumbsAppendExtension | undefined + >(undefined); const badge$ = new BehaviorSubject(undefined); const customNavLink$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); @@ -225,6 +234,7 @@ export class ChromeService { badge$={badge$.pipe(takeUntil(this.stop$))} basePath={http.basePath} breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))} + breadcrumbsAppendExtension$={breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$))} customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} kibanaDocLink={docLinks.links.kibana} forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()} @@ -290,6 +300,14 @@ export class ChromeService { breadcrumbs$.next(newBreadcrumbs); }, + getBreadcrumbsAppendExtension$: () => breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$)), + + setBreadcrumbsAppendExtension: ( + breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension + ) => { + breadcrumbsAppendExtension$.next(breadcrumbsAppendExtension); + }, + getHelpExtension$: () => helpExtension$.pipe(takeUntil(this.stop$)), setHelpExtension: (helpExtension?: ChromeHelpExtension) => { @@ -431,6 +449,18 @@ export interface ChromeStart { */ setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + /** + * Get an observable of the current extension appended to breadcrumbs + */ + getBreadcrumbsAppendExtension$(): Observable; + + /** + * Mount an element next to the last breadcrumb + */ + setBreadcrumbsAppendExtension( + breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension + ): void; + /** * Get an observable of the current custom nav link */ diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 34c5f213a7221..ee2fcbd5078af 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -300,6 +300,55 @@ exports[`Header renders 1`] = ` "thrownError": null, } } + breadcrumbsAppendExtension$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -5029,6 +5078,55 @@ exports[`Header renders 1`] = ` "thrownError": null, } } + breadcrumbsAppendExtension$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } > ; badge$: Observable; breadcrumbs$: Observable; + breadcrumbsAppendExtension$: Observable; customNavLink$: Observable; homeHref: string; isVisible$: Observable; @@ -169,6 +170,7 @@ export function Header({ diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 7fe2c91087090..64401171d142a 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -22,12 +22,17 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { BehaviorSubject } from 'rxjs'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; +import { ChromeBreadcrumbsAppendExtension } from '../../chrome_service'; describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); const wrapper = mount( - + ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); @@ -39,4 +44,29 @@ describe('HeaderBreadcrumbs', () => { wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); }); + + it('renders breadcrumbs extension', () => { + const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); + const breadcrumbsAppendExtension$ = new BehaviorSubject< + undefined | ChromeBreadcrumbsAppendExtension + >({ + content: (root: HTMLDivElement) => { + root.innerHTML = '
__render__
'; + return () => (root.innerHTML = ''); + }, + }); + + const wrapper = mount( + + ); + + expect(wrapper.find('.euiBreadcrumb').getDOMNode().querySelector('my-extension')).toBeDefined(); + act(() => breadcrumbsAppendExtension$.next(undefined)); + wrapper.update(); + expect(wrapper.find('.euiBreadcrumb').getDOMNode().querySelector('my-extension')).toBeNull(); + }); }); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 52412f8990c7a..d52faa87cfecd 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -22,16 +22,19 @@ import classNames from 'classnames'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { ChromeBreadcrumb } from '../../chrome_service'; +import { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from '../../chrome_service'; +import { HeaderExtension } from './header_extension'; interface Props { appTitle$: Observable; breadcrumbs$: Observable; + breadcrumbsAppendExtension$: Observable; } -export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { +export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$, breadcrumbsAppendExtension$ }: Props) { const appTitle = useObservable(appTitle$, 'Kibana'); const breadcrumbs = useObservable(breadcrumbs$, []); + const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$); let crumbs = breadcrumbs; if (breadcrumbs.length === 0 && appTitle) { @@ -48,5 +51,15 @@ export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { ), })); + if (breadcrumbsAppendExtension) { + const lastCrumb = crumbs[crumbs.length - 1]; + lastCrumb.text = ( + <> + {lastCrumb.text} + + + ); + } + return ; } diff --git a/src/core/public/chrome/ui/header/header_extension.test.tsx b/src/core/public/chrome/ui/header/header_extension.test.tsx index 3d5678b8bb7ef..ba00c74b81cfa 100644 --- a/src/core/public/chrome/ui/header/header_extension.test.tsx +++ b/src/core/public/chrome/ui/header/header_extension.test.tsx @@ -32,6 +32,14 @@ describe('HeaderExtension', () => { expect(divNode).toBeInstanceOf(HTMLElement); }); + it('calls navControl.render with div node as inlineBlock', () => { + const renderSpy = jest.fn(); + mount(); + + const [divNode] = renderSpy.mock.calls[0]; + expect(divNode).toHaveAttribute('style', 'display: inline-block;'); + }); + it('calls unrender callback when unmounted', () => { const unrenderSpy = jest.fn(); const render = () => unrenderSpy; diff --git a/src/core/public/chrome/ui/header/header_extension.tsx b/src/core/public/chrome/ui/header/header_extension.tsx index 76413a0ea0317..97cf38f44c3f1 100644 --- a/src/core/public/chrome/ui/header/header_extension.tsx +++ b/src/core/public/chrome/ui/header/header_extension.tsx @@ -22,6 +22,7 @@ import { MountPoint } from '../../../types'; interface Props { extension?: MountPoint; + display?: 'block' | 'inlineBlock'; } export class HeaderExtension extends React.Component { @@ -46,7 +47,12 @@ export class HeaderExtension extends React.Component { } public render() { - return
; + return ( +
+ ); } private renderExtension() { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f52d5b6fbd6a5..4babec38a936e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -343,6 +343,8 @@ export interface ChromeStart { getBadge$(): Observable; getBrand$(): Observable; getBreadcrumbs$(): Observable; + // Warning: (ae-forgotten-export) The symbol "ChromeBreadcrumbsAppendExtension" needs to be exported by the entry point index.d.ts + getBreadcrumbsAppendExtension$(): Observable; getCustomNavLink$(): Observable | undefined>; getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; @@ -355,6 +357,7 @@ export interface ChromeStart { setBadge(badge?: ChromeBadge): void; setBrand(brand: ChromeBrand): void; setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + setBreadcrumbsAppendExtension(breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension): void; setCustomNavLink(newCustomNavLink?: Partial): void; setHelpExtension(helpExtension?: ChromeHelpExtension): void; setHelpSupportUrl(url: string): void;