From 0503f873f8de1790e82f7adfc3bdd758c91f97bd Mon Sep 17 00:00:00 2001 From: Denis Maximov Date: Tue, 10 Nov 2020 12:25:28 +0200 Subject: [PATCH 01/19] [Lens] Do not reset formatting when switching between custom ranges and auto histogram (#82694) --- .../definitions/ranges/ranges.test.tsx | 30 +++++++++++++++++++ .../operations/definitions/ranges/ranges.tsx | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index c8a8ffa7b128f..d43a905434c02 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -744,6 +744,36 @@ describe('ranges', () => { /^Bytes format:/ ); }); + + it('should not reset formatters when switching between custom ranges and auto histogram', () => { + const setStateSpy = jest.fn(); + // now set a format on the range operation + (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.format = { + id: 'custom', + params: { decimals: 3 }, + }; + + const instance = mount( + + ); + + // This series of act closures are made to make it work properly the update flush + act(() => { + instance.find(EuiLink).first().prop('onClick')!({} as ReactMouseEvent); + }); + + expect(setStateSpy.mock.calls[1][0].layers.first.columns.col1.params.format).toEqual({ + id: 'custom', + params: { decimals: 3 }, + }); + }); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 1050eef45a71c..46d9e4e6c22de 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -225,7 +225,7 @@ export const rangeOperation: OperationDefinition Date: Tue, 10 Nov 2020 13:24:07 +0200 Subject: [PATCH 02/19] [Visualizations] Make the icon buttons labels more descriptive (#82585) * [Visualizations] Make the icon buttons labels more descriptive on the Vis Editor * Fix jest test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/agg.test.tsx.snap | 4 ++-- .../public/components/agg.tsx | 19 +++++++++++++------ .../public/components/agg_add.tsx | 14 +++++++++----- .../translations/translations/ja-JP.json | 5 ----- .../translations/translations/zh-CN.json | 5 ----- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap index 2a521bc01219c..26173cddb3716 100644 --- a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap +++ b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap @@ -17,12 +17,12 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` extraAction={
{ const actionIcons = []; + const aggTitle = agg.type?.title?.toLowerCase(); if (showError) { actionIcons.push({ @@ -170,7 +172,8 @@ function DefaultEditorAgg({ color: 'danger', type: 'alert', tooltip: i18n.translate('visDefaultEditor.agg.errorsAriaLabel', { - defaultMessage: 'Aggregation has errors', + defaultMessage: '{schemaTitle} {aggTitle} aggregation has errors', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'hasErrorsAggregationIcon', }); @@ -184,7 +187,8 @@ function DefaultEditorAgg({ type: 'eye', onClick: () => onToggleEnableAgg(agg.id, false), tooltip: i18n.translate('visDefaultEditor.agg.disableAggButtonTooltip', { - defaultMessage: 'Disable aggregation', + defaultMessage: 'Disable {schemaTitle} {aggTitle} aggregation', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'toggleDisableAggregationBtn disable', }); @@ -196,7 +200,8 @@ function DefaultEditorAgg({ type: 'eyeClosed', onClick: () => onToggleEnableAgg(agg.id, true), tooltip: i18n.translate('visDefaultEditor.agg.enableAggButtonTooltip', { - defaultMessage: 'Enable aggregation', + defaultMessage: 'Enable {schemaTitle} {aggTitle} aggregation', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'toggleDisableAggregationBtn enable', }); @@ -206,7 +211,8 @@ function DefaultEditorAgg({ id: 'dragHandle', type: 'grab', tooltip: i18n.translate('visDefaultEditor.agg.modifyPriorityButtonTooltip', { - defaultMessage: 'Modify priority by dragging', + defaultMessage: 'Modify priority of {schemaTitle} {aggTitle} by dragging', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'dragHandleBtn', }); @@ -218,7 +224,8 @@ function DefaultEditorAgg({ type: 'cross', onClick: () => removeAgg(agg.id), tooltip: i18n.translate('visDefaultEditor.agg.removeDimensionButtonTooltip', { - defaultMessage: 'Remove dimension', + defaultMessage: 'Remove {schemaTitle} {aggTitle} aggregation', + values: { aggTitle, schemaTitle }, }), dataTestSubj: 'removeDimensionBtn', }); @@ -257,7 +264,7 @@ function DefaultEditorAgg({
); }; - const schemaTitle = getSchemaByName(schemas, agg.schema).title; + const buttonContent = ( <> {schemaTitle || agg.schema} {showDescription && {aggDescription}} diff --git a/src/plugins/vis_default_editor/public/components/agg_add.tsx b/src/plugins/vis_default_editor/public/components/agg_add.tsx index 46d5af8cec680..e78f2fcc4453c 100644 --- a/src/plugins/vis_default_editor/public/components/agg_add.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_add.tsx @@ -56,22 +56,26 @@ function DefaultEditorAggAdd({ addSchema(schema); }; + const groupNameLabel = + groupName === AggGroupNames.Buckets + ? i18n.translate('visDefaultEditor.aggAdd.bucketLabel', { defaultMessage: 'bucket' }) + : i18n.translate('visDefaultEditor.aggAdd.metricLabel', { defaultMessage: 'metric' }); + const addButton = ( setIsPopoverOpen(!isPopoverOpen)} + aria-label={i18n.translate('visDefaultEditor.aggAdd.addGroupButtonLabel', { + defaultMessage: 'Add {groupNameLabel}', + values: { groupNameLabel }, + })} > ); - const groupNameLabel = - groupName === AggGroupNames.Buckets - ? i18n.translate('visDefaultEditor.aggAdd.bucketLabel', { defaultMessage: 'bucket' }) - : i18n.translate('visDefaultEditor.aggAdd.metricLabel', { defaultMessage: 'metric' }); - const isSchemaDisabled = (schema: Schema): boolean => { const count = group.filter((agg) => agg.schema === schema.name).length; return count >= schema.max; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d8e136567564e..d9a113b69b473 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3542,11 +3542,6 @@ "uiActions.triggers.valueClickTitle": "シングルクリック", "usageCollection.stats.notReadyMessage": "まだ統計が準備できていません。しばらくたってから再試行してください。", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高度な設定", - "visDefaultEditor.agg.disableAggButtonTooltip": "集約を無効にする", - "visDefaultEditor.agg.enableAggButtonTooltip": "集約を有効にする", - "visDefaultEditor.agg.errorsAriaLabel": "集約にエラーがあります", - "visDefaultEditor.agg.modifyPriorityButtonTooltip": "ドラッグして優先順位を変更します", - "visDefaultEditor.agg.removeDimensionButtonTooltip": "次元を削除", "visDefaultEditor.agg.toggleEditorButtonAriaLabel": "{schema} エディターを切り替える", "visDefaultEditor.aggAdd.addButtonLabel": "追加", "visDefaultEditor.aggAdd.addGroupButtonLabel": "{groupNameLabel} を追加", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 84cc3510ce487..4a393f8a7a223 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3543,11 +3543,6 @@ "uiActions.triggers.valueClickTitle": "单击", "usageCollection.stats.notReadyMessage": "统计信息尚未准备就绪。请稍后重试。", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高级", - "visDefaultEditor.agg.disableAggButtonTooltip": "禁用聚合", - "visDefaultEditor.agg.enableAggButtonTooltip": "启用聚合", - "visDefaultEditor.agg.errorsAriaLabel": "聚合有错误", - "visDefaultEditor.agg.modifyPriorityButtonTooltip": "通过拖动来修改优先级", - "visDefaultEditor.agg.removeDimensionButtonTooltip": "移除维度", "visDefaultEditor.agg.toggleEditorButtonAriaLabel": "切换 {schema} 编辑器", "visDefaultEditor.aggAdd.addButtonLabel": "添加", "visDefaultEditor.aggAdd.addGroupButtonLabel": "添加{groupNameLabel}", From 471c281fa2572860380d06b26420d694445d1e75 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 10 Nov 2020 13:34:05 +0200 Subject: [PATCH 03/19] [Visualizations] Remove kui usage (#82810) * [Visualize] Remove kui usage * Use EuiPromptButton istead of EuiCallout * Add a link to advanced settings for visualizations docs * Changes requested on code review Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/doc_links/doc_links_service.ts | 1 + .../public/embeddable/_index.scss | 1 - .../embeddable/_visualize_lab_disabled.scss | 13 ----- .../embeddable/disabled_lab_visualization.tsx | 51 ++++++++++++------- 4 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 src/plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 48187fe465392..0815df4b9b0c7 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -131,6 +131,7 @@ export class DocLinksService { management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, + visualizationSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-visualization-settings`, }, visualize: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`, diff --git a/src/plugins/visualizations/public/embeddable/_index.scss b/src/plugins/visualizations/public/embeddable/_index.scss index c1e3809657bfa..9703e90159f48 100644 --- a/src/plugins/visualizations/public/embeddable/_index.scss +++ b/src/plugins/visualizations/public/embeddable/_index.scss @@ -1,2 +1 @@ -@import 'visualize_lab_disabled'; @import 'embeddables'; diff --git a/src/plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss b/src/plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss deleted file mode 100644 index 914480ff8c777..0000000000000 --- a/src/plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss +++ /dev/null @@ -1,13 +0,0 @@ -.visDisabledLabVisualization { - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; -} - -.visDisabledLabVisualization__icon { - font-size: $euiFontSizeXL; -} - diff --git a/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx b/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx index 3d2af2c591a3c..ea7760f31d54c 100644 --- a/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx +++ b/src/plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx @@ -17,29 +17,42 @@ * under the License. */ -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import React from 'react'; +import { getDocLinks } from '../services'; export function DisabledLabVisualization({ title }: { title: string }) { + const advancedSettingsLink = getDocLinks().links.management.visualizationSettings; return ( -
- + ); } From a7a83fd4215803d8b835bd4a97a8db4559ef3703 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 10 Nov 2020 13:42:12 +0200 Subject: [PATCH 04/19] [TSVB] Disable using top_hits in pipeline aggregations (#82278) * [TSVB] Disable using top_hits in bucket script aggregations * remove console message * Correct schema for metrics size * Chanhe hardcoded agg with the exported for the METRIC_TYPES var * Exclude top_hit agg from all Sibling Pipeline Aggregations and all Parent Pipeline Aggregations Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_type_timeseries/common/vis_schema.ts | 2 +- .../public/application/components/aggs/calculation.js | 2 ++ .../public/application/components/aggs/cumulative_sum.js | 2 ++ .../public/application/components/aggs/derivative.js | 2 ++ .../public/application/components/aggs/moving_average.js | 2 ++ .../public/application/components/aggs/positive_only.js | 2 ++ .../public/application/components/aggs/serial_diff.js | 2 ++ .../public/application/components/aggs/std_sibling.js | 5 ++++- .../public/application/components/aggs/vars.js | 1 + 9 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 27f09fb574b0f..9ec5ae1424ae3 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -120,7 +120,7 @@ export const metricsItems = schema.object({ type: stringRequired, value: stringOptionalNullable, values: schema.maybe(schema.nullable(schema.arrayOf(schema.nullable(schema.string())))), - size: stringOptionalNullable, + size: stringOrNumberOptionalNullable, agg_with: stringOptionalNullable, order: stringOptionalNullable, order_by: stringOptionalNullable, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js index bb3d39797656f..5bf4fb55ee5e5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js @@ -26,6 +26,7 @@ import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createTextHandler } from '../lib/create_text_handler'; import { CalculationVars, newVariable } from './vars'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -99,6 +100,7 @@ export function CalculationAgg(props) { onChange={handleChange} name="variables" model={model} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js index 11b3e303e7e00..0b879adbd37ae 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js @@ -24,6 +24,7 @@ import { AggSelect } from './agg_select'; import { MetricSelect } from './metric_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { FormattedMessage } from '@kbn/i18n/react'; import { htmlIdGenerator, @@ -80,6 +81,7 @@ export function CumulativeSumAgg(props) { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index faf1a59adc4aa..fa1289dc74c72 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -25,6 +25,7 @@ import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createTextHandler } from '../lib/create_text_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -91,6 +92,7 @@ export const DerivativeAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} fullWidth /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index 316e0f9af43bd..fb945d2606bc8 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -25,6 +25,7 @@ import { MetricSelect } from './metric_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createNumberHandler } from '../lib/create_number_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -153,6 +154,7 @@ export const MovingAverageAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 1999862f7aa0e..6ca5fa8e7447f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -24,6 +24,7 @@ import { MetricSelect } from './metric_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -85,6 +86,7 @@ export const PositiveOnlyAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index 10b3d551bb89f..e3a0c74273539 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -25,6 +25,7 @@ import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createNumberHandler } from '../lib/create_number_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; import { htmlIdGenerator, EuiFlexGroup, @@ -87,6 +88,7 @@ export const SerialDiffAgg = (props) => { metrics={siblings} metric={model} value={model.field} + exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index 30e5c57ac90ba..bed5e9caa9f87 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -25,6 +25,8 @@ import { AggSelect } from './agg_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; import { createTextHandler } from '../lib/create_text_handler'; +import { METRIC_TYPES } from '../../../../common/metric_types'; + import { htmlIdGenerator, EuiFlexGroup, @@ -154,7 +156,7 @@ const StandardSiblingAggUi = (props) => { > From 446cffeccf9a19baee96aaf4b146547a3f7f721a Mon Sep 17 00:00:00 2001 From: Bill McConaghy Date: Tue, 10 Nov 2020 07:21:52 -0500 Subject: [PATCH 05/19] renaming built-in alerts to Stack Alerts (#82873) * renaming built-in alerts to Stack Alerts * responding to PR feedback and adding glossary definition for stack alerts * Update docs/glossary.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/glossary.asciidoc | 5 +++++ docs/user/alerting/alert-types.asciidoc | 8 +++++--- docs/user/alerting/alerting-getting-started.asciidoc | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index be24402170bbe..ff03a60173961 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -330,6 +330,11 @@ See {kibana-ref}/xpack-spaces.html[Spaces]. // end::space-def[] +[[glossary-stack-alerts]] stack alerts :: +// tag::stack-alert-def[] +The general purpose alert types {kib} provides out of the box. Index threshold and geo alerts are currently the two stack alert types. +// end::stack-alert-def[] + [float] [[t_glos]] diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index f71e43c5defc7..7de5ff56228cc 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -2,11 +2,13 @@ [[alert-types]] == Alert types -{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. +{kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. -This section covers built-in alert types. For domain-specific alert types, refer to the documentation for that app. +This section covers stack alerts. For domain-specific alert types, refer to the documentation for that app. +Users will need `all` access to the *Stack Alerts* feature to be able to create and edit any of the alerts listed below. +See <> for more information on configuring roles that provide access to this feature. -Currently {kib} provides one built-in alert type: the <> type. +Currently {kib} provides one stack alert: the <> type. [float] [[alert-type-index-threshold]] diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 2b22b49375676..53aef4aaa062e 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -6,7 +6,7 @@ beta[] -- -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> (known as stack alerts) for you to use. image::images/alerting-overview.png[Alerts and actions UI] From 451e387f40d211d1eb36589c7d8de95d4e6e94b6 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 10 Nov 2020 07:37:44 -0500 Subject: [PATCH 06/19] Fix SO query for searching across spaces (#83025) --- .../service/lib/search_dsl/query_params.test.ts | 5 ++--- .../service/lib/search_dsl/query_params.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index c35ec809fcf8d..e78b944183df9 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -103,12 +103,11 @@ describe('#getQueryParams', () => { if (registry.isMultiNamespace(type)) { const array = [...(namespaces ?? [DEFAULT_NAMESPACE_STRING]), ALL_NAMESPACES_STRING]; - const namespacesClause = { terms: { namespaces: array } }; return { bool: { must: namespaces?.includes(ALL_NAMESPACES_STRING) - ? expect.not.arrayContaining([namespacesClause]) - : expect.arrayContaining([namespacesClause]), + ? [{ term: { type } }] + : [{ term: { type } }, { terms: { namespaces: array } }], must_not: [{ exists: { field: 'namespace' } }], }, }; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 2ecba42e408e7..cb58db171681a 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -78,13 +78,19 @@ function getClauseForType( const searchAcrossAllNamespaces = namespaces.includes(ALL_NAMESPACES_STRING); if (registry.isMultiNamespace(type)) { - const namespacesFilterClause = searchAcrossAllNamespaces - ? {} - : { terms: { namespaces: [...namespaces, ALL_NAMESPACES_STRING] } }; + const typeFilterClause = { term: { type } }; + + const namespacesFilterClause = { + terms: { namespaces: [...namespaces, ALL_NAMESPACES_STRING] }, + }; + + const must = searchAcrossAllNamespaces + ? [typeFilterClause] + : [typeFilterClause, namespacesFilterClause]; return { bool: { - must: [{ term: { type } }, namespacesFilterClause], + must, must_not: [{ exists: { field: 'namespace' } }], }, }; From 6003cadce4abfa19dfec1d08b037d45ba11367d7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 10 Nov 2020 12:43:39 +0000 Subject: [PATCH 07/19] skip flaky suite (#82804) --- .../security_and_spaces/tests/alerting/update.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 0ad2ca226ed5d..8836bc2e4db2f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -31,7 +31,8 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .then((response: SupertestResponse) => response.body); } - describe('update', () => { + // FLAKY: https://github.com/elastic/kibana/issues/82804 + describe.skip('update', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); From 0b99841310b804901827040aaf025468941710a3 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 10 Nov 2020 14:31:04 +0100 Subject: [PATCH 08/19] [Lens] Performance refactoring for indexpattern fast lookup and Operation support matrix computation (#82829) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../datapanel.test.tsx | 254 +++++++------- .../indexpattern_datasource/datapanel.tsx | 5 +- .../bucket_nesting_editor.test.tsx | 22 +- .../dimension_panel/bucket_nesting_editor.tsx | 15 +- .../dimension_panel/dimension_editor.tsx | 45 ++- .../dimension_panel/dimension_panel.test.tsx | 74 ++-- .../dimension_panel/droppable.test.ts | 91 +++-- .../dimension_panel/droppable.ts | 6 +- .../dimension_panel/field_select.tsx | 25 +- .../dimension_panel/operation_support.ts | 38 +- .../indexpattern.test.ts | 205 +++++------ .../indexpattern_suggestions.test.tsx | 326 ++++++++++-------- .../indexpattern_suggestions.ts | 32 +- .../layerpanel.test.tsx | 232 +++++++------ .../indexpattern_datasource/loader.test.ts | 13 +- .../public/indexpattern_datasource/loader.ts | 2 + .../public/indexpattern_datasource/mocks.ts | 117 ++++--- .../operations/definitions/cardinality.tsx | 2 +- .../definitions/date_histogram.test.tsx | 79 ++++- .../operations/definitions/date_histogram.tsx | 19 +- .../operations/definitions/metrics.tsx | 2 +- .../definitions/ranges/ranges.test.tsx | 4 + .../operations/definitions/ranges/ranges.tsx | 6 +- .../operations/definitions/terms/index.tsx | 2 +- .../definitions/terms/terms.test.tsx | 2 +- .../operations/operations.test.ts | 50 +-- .../indexpattern_datasource/pure_helpers.ts | 8 +- .../state_helpers.test.ts | 101 +++--- .../public/indexpattern_datasource/types.ts | 1 + .../public/indexpattern_datasource/utils.ts | 10 +- 30 files changed, 996 insertions(+), 792 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index c48bc3dc52404..d2ec1c81bbeec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -18,6 +18,131 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { getFieldByNameFactory } from './pure_helpers'; + +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'amemory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'client', + displayName: 'client', + type: 'ip', + aggregatable: true, + searchable: true, + }, + documentField, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + histogram: { + agg: 'histogram', + interval: 1000, + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, + documentField, +]; + +const fieldsThree = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + documentField, +]; const initialState: IndexPatternPrivateState = { indexPatternRefs: [], @@ -85,139 +210,24 @@ const initialState: IndexPatternPrivateState = { title: 'idx1', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - displayName: 'amemory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'unsupported', - displayName: 'unsupported', - type: 'geo', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'client', - displayName: 'client', - type: 'ip', - aggregatable: true, - searchable: true, - }, - documentField, - ], + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, '2': { id: '2', title: 'idx2', timeFieldName: 'timestamp', hasRestrictions: true, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - histogram: { - agg: 'histogram', - interval: 1000, - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - documentField, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, '3': { id: '3', title: 'idx3', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - documentField, - ], + fields: fieldsThree, + getFieldByName: getFieldByNameFactory(fieldsThree), }, }, isFirstExistenceFetch: false, @@ -330,6 +340,7 @@ describe('IndexPattern Data Panel', () => { title: 'aaa', timeFieldName: 'atime', fields: [], + getFieldByName: getFieldByNameFactory([]), hasRestrictions: false, }, b: { @@ -337,6 +348,7 @@ describe('IndexPattern Data Panel', () => { title: 'bbb', timeFieldName: 'btime', fields: [], + getFieldByName: getFieldByNameFactory([]), hasRestrictions: false, }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 28c5605f3bfc5..f2c7d7fc20926 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -5,7 +5,7 @@ */ import './datapanel.scss'; -import { uniq, keyBy, groupBy } from 'lodash'; +import { uniq, groupBy } from 'lodash'; import React, { useState, memo, useCallback, useMemo } from 'react'; import { EuiFlexGroup, @@ -266,9 +266,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions; const unfilteredFieldGroups: FieldGroups = useMemo(() => { - const fieldByName = keyBy(allFields, 'name'); const containsData = (field: IndexPatternField) => { - const overallField = fieldByName[field.name]; + const overallField = currentIndexPattern.getFieldByName(field.name); return ( overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx index 3696f3ad7b102..ee6a86072236c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx @@ -10,12 +10,14 @@ import { BucketNestingEditor } from './bucket_nesting_editor'; import { IndexPatternColumn } from '../indexpattern'; import { IndexPatternField } from '../types'; -const fieldMap = { +const fieldMap: Record = { a: { displayName: 'a' } as IndexPatternField, b: { displayName: 'b' } as IndexPatternField, c: { displayName: 'c' } as IndexPatternField, }; +const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name]; + describe('BucketNestingEditor', () => { function mockCol(col: Partial = {}): IndexPatternColumn { const result = { @@ -39,7 +41,7 @@ describe('BucketNestingEditor', () => { it('should display the top level grouping when at the root', () => { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const setColumns = jest.fn(); const component = mount( , column: IndexPatternColumn) { - return hasField(column) ? fieldMap[column.sourceField]?.displayName || column.sourceField : ''; +function getFieldName( + column: IndexPatternColumn, + getFieldByName: (name: string) => IndexPatternField | undefined +) { + return hasField(column) + ? getFieldByName(column.sourceField)?.displayName || column.sourceField + : ''; } export function BucketNestingEditor({ columnId, layer, setColumns, - fieldMap, + getFieldByName, }: { columnId: string; layer: IndexPatternLayer; setColumns: (columns: string[]) => void; - fieldMap: Record; + getFieldByName: (name: string) => IndexPatternField | undefined; }) { const column = layer.columns[columnId]; const columns = Object.entries(layer.columns); @@ -42,7 +47,7 @@ export function BucketNestingEditor({ .map(([value, c]) => ({ value, text: c.label, - fieldName: getFieldName(fieldMap, c), + fieldName: getFieldName(c, getFieldByName), operationType: c.operationType, })); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index a18cb69db74cb..7cbfbc1749382 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -29,7 +29,7 @@ import { deleteColumn, changeColumn, updateColumnParam, mergeLayer } from '../st import { FieldSelect } from './field_select'; import { hasField, fieldIsInvalid } from '../utils'; import { BucketNestingEditor } from './bucket_nesting_editor'; -import { IndexPattern, IndexPatternField } from '../types'; +import { IndexPattern } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { FormatSelector } from './format_selector'; @@ -97,23 +97,13 @@ export function DimensionEditor(props: DimensionEditorProps) { const ParamEditor = selectedOperationDefinition?.paramEditor; - const fieldMap: Record = useMemo(() => { - const fields: Record = {}; - currentIndexPattern.fields.forEach((field) => { - fields[field.name] = field; - }); - return fields; - }, [currentIndexPattern]); - const possibleOperations = useMemo(() => { return Object.values(operationDefinitionMap) .sort((op1, op2) => { return op1.displayName.localeCompare(op2.displayName); }) .map((def) => def.type) - .filter( - (type) => fieldByOperation[type]?.length || operationWithoutField.indexOf(type) !== -1 - ); + .filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type)); }, [fieldByOperation, operationWithoutField]); // Operations are compatible if they match inputs. They are always compatible in @@ -128,7 +118,7 @@ export function DimensionEditor(props: DimensionEditorProps) { (selectedColumn && hasField(selectedColumn) && definition.input === 'field' && - fieldByOperation[operationType]?.indexOf(selectedColumn.sourceField) !== -1) || + fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || (selectedColumn && !hasField(selectedColumn) && definition.input !== 'field'), }; }); @@ -198,9 +188,9 @@ export function DimensionEditor(props: DimensionEditorProps) { trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } else if (!selectedColumn || !compatibleWithCurrentField) { - const possibleFields = fieldByOperation[operationType] || []; + const possibleFields = fieldByOperation[operationType] || new Set(); - if (possibleFields.length === 1) { + if (possibleFields.size === 1) { setState( changeColumn({ state, @@ -212,7 +202,7 @@ export function DimensionEditor(props: DimensionEditorProps) { layerId: props.layerId, op: operationType, indexPattern: currentIndexPattern, - field: fieldMap[possibleFields[0]], + field: currentIndexPattern.getFieldByName(possibleFields.values().next().value), previousColumn: selectedColumn, }), }) @@ -236,7 +226,9 @@ export function DimensionEditor(props: DimensionEditorProps) { layerId: props.layerId, op: operationType, indexPattern: currentIndexPattern, - field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined, + field: hasField(selectedColumn) + ? currentIndexPattern.getFieldByName(selectedColumn.sourceField) + : undefined, previousColumn: selectedColumn, }); @@ -297,7 +289,6 @@ export function DimensionEditor(props: DimensionEditorProps) { fieldIsInvalid={currentFieldIsInvalid} currentIndexPattern={currentIndexPattern} existingFields={state.existingFields} - fieldMap={fieldMap} operationSupportMatrix={operationSupportMatrix} selectedColumnOperationType={selectedColumn && selectedColumn.operationType} selectedColumnSourceField={ @@ -323,25 +314,29 @@ export function DimensionEditor(props: DimensionEditorProps) { ) { // If we just changed the field are not in an error state and the operation didn't change, // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + column = changeField( + selectedColumn, + currentIndexPattern, + currentIndexPattern.getFieldByName(choice.field)! + ); } else { // Otherwise we'll use the buildColumn method to calculate a new column const compatibleOperations = ('field' in choice && operationSupportMatrix.operationByField[choice.field]) || - []; + new Set(); let operation; - if (compatibleOperations.length > 0) { + if (compatibleOperations.size > 0) { operation = incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) + compatibleOperations.has(incompatibleSelectedOperationType) ? incompatibleSelectedOperationType - : compatibleOperations[0]; + : compatibleOperations.values().next().value; } else if ('field' in choice) { operation = choice.operationType; } column = buildColumn({ columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], + field: currentIndexPattern.getFieldByName(choice.field), indexPattern: currentIndexPattern, layerId: props.layerId, suggestedPriority: props.suggestedPriority, @@ -417,12 +412,12 @@ export function DimensionEditor(props: DimensionEditorProps) { {!incompatibleSelectedOperationType && !hideGrouping && ( setState(mergeLayer({ state, layerId, newLayer: { columnOrder } })) } + getFieldByName={currentIndexPattern.getFieldByName} /> )} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 92a4dad14dd25..3ed04b08df58f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -22,6 +22,7 @@ import { IndexPatternColumn } from '../operations'; import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; +import { getFieldByNameFactory } from '../pure_helpers'; jest.mock('../loader'); jest.mock('../state_helpers'); @@ -34,6 +35,42 @@ jest.mock('lodash', () => { }; }); +const fields = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, +]; + const expectedIndexPatterns = { 1: { id: '1', @@ -41,41 +78,8 @@ const expectedIndexPatterns = { timeFieldName: 'timestamp', hasExistence: true, hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - documentField, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index dd696f8be357f..1d85c1f8f78ca 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -15,9 +15,46 @@ import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { IndexPatternColumn } from '../operations'; +import { getFieldByNameFactory } from '../pure_helpers'; jest.mock('../state_helpers'); +const fields = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, +]; + const expectedIndexPatterns = { 1: { id: '1', @@ -25,41 +62,8 @@ const expectedIndexPatterns = { timeFieldName: 'timestamp', hasExistence: true, hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - documentField, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }; @@ -177,6 +181,23 @@ describe('IndexPatternDimensionEditorPanel', () => { type: 'string', }, ], + + getFieldByName: getFieldByNameFactory([ + { + aggregatable: true, + name: 'bar', + displayName: 'bar', + searchable: true, + type: 'number', + }, + { + aggregatable: true, + name: 'mystring', + displayName: 'mystring', + searchable: true, + type: 'string', + }, + ]), }, }, currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index 7f509cd0244f0..a6ff550af96e9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -137,9 +137,9 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps { currentIndexPattern: IndexPattern; - fieldMap: Record; incompatibleSelectedOperationType: OperationType | null; selectedColumnOperationType?: OperationType; selectedColumnSourceField?: string; @@ -46,7 +45,6 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { export function FieldSelect({ currentIndexPattern, - fieldMap, incompatibleSelectedOperationType, selectedColumnOperationType, selectedColumnSourceField, @@ -63,31 +61,32 @@ export function FieldSelect({ function isCompatibleWithCurrentOperation(fieldName: string) { if (incompatibleSelectedOperationType) { - return operationByField[fieldName]!.includes(incompatibleSelectedOperationType); + return operationByField[fieldName]!.has(incompatibleSelectedOperationType); } return ( !selectedColumnOperationType || - operationByField[fieldName]!.includes(selectedColumnOperationType) + operationByField[fieldName]!.has(selectedColumnOperationType) ); } const [specialFields, normalFields] = _.partition( fields, - (field) => fieldMap[field].type === 'document' + (field) => currentIndexPattern.getFieldByName(field)?.type === 'document' ); const containsData = (field: string) => - fieldMap[field].type === 'document' || + currentIndexPattern.getFieldByName(field)?.type === 'document' || fieldExists(existingFields, currentIndexPattern.title, field); function fieldNamesToOptions(items: string[]) { return items + .filter((field) => currentIndexPattern.getFieldByName(field)?.displayName) .map((field) => ({ - label: fieldMap[field].displayName, + label: currentIndexPattern.getFieldByName(field)?.displayName, value: { type: 'field', field, - dataType: fieldMap[field].type, + dataType: currentIndexPattern.getFieldByName(field)?.type, operationType: selectedColumnOperationType && isCompatibleWithCurrentOperation(field) ? selectedColumnOperationType @@ -118,7 +117,10 @@ export function FieldSelect({ })); } - const [metaFields, nonMetaFields] = _.partition(normalFields, (field) => fieldMap[field].meta); + const [metaFields, nonMetaFields] = _.partition( + normalFields, + (field) => currentIndexPattern.getFieldByName(field)?.meta + ); const [availableFields, emptyFields] = _.partition(nonMetaFields, containsData); const constructFieldsOptions = (fieldsArr: string[], label: string) => @@ -158,7 +160,6 @@ export function FieldSelect({ incompatibleSelectedOperationType, selectedColumnOperationType, currentIndexPattern, - fieldMap, operationByField, existingFields, ]); @@ -180,7 +181,7 @@ export function FieldSelect({ { label: fieldIsInvalid ? selectedColumnSourceField - : fieldMap[selectedColumnSourceField]?.displayName, + : currentIndexPattern.getFieldByName(selectedColumnSourceField)?.displayName, value: { type: 'field', field: selectedColumnSourceField }, }, ] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 2ea28da201556..31fb5277d53ec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -11,9 +11,9 @@ import { getAvailableOperationsByMetadata } from '../operations'; import { IndexPatternPrivateState } from '../types'; export interface OperationSupportMatrix { - operationByField: Partial>; - operationWithoutField: OperationType[]; - fieldByOperation: Partial>; + operationByField: Partial>>; + operationWithoutField: Set; + fieldByOperation: Partial>>; } type Props = Pick< @@ -31,30 +31,30 @@ export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix currentIndexPattern ).filter((operation) => props.filterOperations(operation.operationMetaData)); - const supportedOperationsByField: Partial> = {}; - const supportedOperationsWithoutField: OperationType[] = []; - const supportedFieldsByOperation: Partial> = {}; + const supportedOperationsByField: Partial>> = {}; + const supportedOperationsWithoutField: Set = new Set(); + const supportedFieldsByOperation: Partial>> = {}; filteredOperationsByMetadata.forEach(({ operations }) => { operations.forEach((operation) => { if (operation.type === 'field') { - supportedOperationsByField[operation.field] = [ - ...(supportedOperationsByField[operation.field] ?? []), - operation.operationType, - ]; - - supportedFieldsByOperation[operation.operationType] = [ - ...(supportedFieldsByOperation[operation.operationType] ?? []), - operation.field, - ]; + if (!supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field] = new Set(); + } + supportedOperationsByField[operation.field]?.add(operation.operationType); + + if (!supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType] = new Set(); + } + supportedFieldsByOperation[operation.operationType]?.add(operation.field); } else if (operation.type === 'none') { - supportedOperationsWithoutField.push(operation.operationType); + supportedOperationsWithoutField.add(operation.operationType); } }); }); return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - operationWithoutField: _.uniq(supportedOperationsWithoutField), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + operationByField: supportedOperationsByField, + operationWithoutField: supportedOperationsWithoutField, + fieldByOperation: supportedFieldsByOperation, }; }; 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 a3f48b162475a..51d95245adb25 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -12,121 +12,128 @@ import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./loader'); jest.mock('../id_generator'); -const expectedIndexPatterns = { - 1: { - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'start_date', + displayName: 'start_date', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', }, - { - name: 'start_date', - displayName: 'start_date', - type: 'date', - aggregatable: true, - searchable: true, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + // Ignored in the UI + histogram: { + agg: 'histogram', + interval: 1000, }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, + avg: { + agg: 'avg', }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, + max: { + agg: 'max', }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, + min: { + agg: 'min', }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, + sum: { + agg: 'sum', }, - ], + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, +]; + +const expectedIndexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, 2: { id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', hasRestrictions: true, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - // Ignored in the UI - histogram: { - agg: 'histogram', - interval: 1000, - }, - avg: { - agg: 'avg', - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index c8cb9fcb33ba9..523a1be34ba3d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -12,121 +12,128 @@ import { getDatasourceSuggestionsFromCurrentState, getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./loader'); jest.mock('../id_generator'); -const expectedIndexPatterns = { - 1: { - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'start_date', + displayName: 'start_date', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', }, - { - name: 'start_date', - displayName: 'start_date', - type: 'date', - aggregatable: true, - searchable: true, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + // Ignored in the UI + histogram: { + agg: 'histogram', + interval: 1000, }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, + avg: { + agg: 'avg', }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, + max: { + agg: 'max', }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, + min: { + agg: 'min', }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', }, - ], + }, + }, +]; + +const expectedIndexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, 2: { id: '2', title: 'my-fake-restricted-pattern', hasRestrictions: true, timeFieldName: 'timestamp', - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - // Ignored in the UI - histogram: { - agg: 'histogram', - interval: 1000, - }, - avg: { - agg: 'avg', - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, }; @@ -335,6 +342,15 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), }, }, layers: { @@ -546,6 +562,16 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), }, }, layers: { @@ -1531,6 +1557,43 @@ describe('IndexPattern Data Source suggestions', () => { it('returns simplified versions of table with more than 2 columns', () => { const initialState = testInitialState(); + const fields = [ + { + name: 'field1', + displayName: 'field1', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + displayName: 'field2', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field3', + displayName: 'field3Label', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field4', + displayName: 'field4', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field5', + displayName: 'field5', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]; const state: IndexPatternPrivateState = { indexPatternRefs: [], existingFields: {}, @@ -1540,43 +1603,8 @@ describe('IndexPattern Data Source suggestions', () => { id: '1', title: 'my-fake-index-pattern', hasRestrictions: false, - fields: [ - { - name: 'field1', - displayName: 'field1', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'field2', - displayName: 'field2', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'field3', - displayName: 'field3Label', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'field4', - displayName: 'field4', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'field5', - displayName: 'field5', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }, isFirstExistenceFetch: false, @@ -1700,6 +1728,23 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + displayName: 'field2', + type: 'date', + aggregatable: true, + searchable: true, + }, + ]), }, }, isFirstExistenceFetch: false, @@ -1756,6 +1801,15 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), }, }, isFirstExistenceFetch: false, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 098569d1f460a..c12d7d4be226b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -128,7 +128,7 @@ export function getDatasourceSuggestionsForVisualizeField( const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); // Identify the field by the indexPatternId and the fieldName const indexPattern = state.indexPatterns[indexPatternId]; - const field = indexPattern.fields.find((fld) => fld.name === fieldName); + const field = indexPattern.getFieldByName(fieldName); if (layerIds.length !== 0 || !field) return []; const newId = generateId(); @@ -371,7 +371,7 @@ function createNewLayerWithMetricAggregation( indexPattern: IndexPattern, field: IndexPatternField ): IndexPatternLayer { - const dateField = indexPattern.fields.find((f) => f.name === indexPattern.timeFieldName)!; + const dateField = indexPattern.getFieldByName(indexPattern.timeFieldName!); const column = getMetricColumn(indexPattern, layerId, field); @@ -451,9 +451,8 @@ export function getDatasourceSuggestionsFromCurrentState( (columnId) => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date' ); - const timeField = indexPattern.fields.find( - ({ name }) => name === indexPattern.timeFieldName - ); + const timeField = + indexPattern.timeFieldName && indexPattern.getFieldByName(indexPattern.timeFieldName); const hasNumericDimension = buckets.length === 1 && @@ -507,17 +506,17 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId const layer = state.layers[layerId]; const [firstBucket, secondBucket, ...rest] = layer.columnOrder; const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] }; - const currentFields = state.indexPatterns[state.currentIndexPatternId].fields; + const indexPattern = state.indexPatterns[state.currentIndexPatternId]; + const firstBucketColumn = layer.columns[firstBucket]; const firstBucketLabel = - currentFields.find((field) => { - const column = layer.columns[firstBucket]; - return hasField(column) && column.sourceField === field.name; - })?.displayName || ''; + (hasField(firstBucketColumn) && + indexPattern.getFieldByName(firstBucketColumn.sourceField)?.displayName) || + ''; + const secondBucketColumn = layer.columns[secondBucket]; const secondBucketLabel = - currentFields.find((field) => { - const column = layer.columns[secondBucket]; - return hasField(column) && column.sourceField === field.name; - })?.displayName || ''; + (hasField(secondBucketColumn) && + indexPattern.getFieldByName(secondBucketColumn.sourceField)?.displayName) || + ''; return buildSuggestion({ state, @@ -604,7 +603,10 @@ function createAlternativeMetricSuggestions( if (!hasField(column)) { return; } - const field = indexPattern.fields.find(({ name }) => column.sourceField === name)!; + const field = indexPattern.getFieldByName(column.sourceField); + if (!field) { + return; + } const alternativeMetricOperations = getOperationTypesForField(field) .map((op) => buildColumn({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 92e35b257f24a..40eb52fe67c6d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -11,6 +11,7 @@ import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; import { ShallowWrapper } from 'enzyme'; import { EuiSelectable } from '@elastic/eui'; import { ChangeIndexPattern } from './change_indexpattern'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./state_helpers'); @@ -19,6 +20,120 @@ interface IndexPatternPickerOption { checked?: 'on' | 'off'; } +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + histogram: { + agg: 'histogram', + interval: 1000, + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, +]; + +const fieldsThree = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + const initialState: IndexPatternPrivateState = { indexPatternRefs: [ { id: '1', title: 'my-fake-index-pattern' }, @@ -63,129 +178,24 @@ const initialState: IndexPatternPrivateState = { title: 'my-fake-index-pattern', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'unsupported', - displayName: 'unsupported', - type: 'geo', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, '2': { id: '2', title: 'my-fake-restricted-pattern', hasRestrictions: true, timeFieldName: 'timestamp', - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - histogram: { - agg: 'histogram', - interval: 1000, - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, '3': { id: '3', title: 'my-compatible-pattern', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], + fields: fieldsThree, + getFieldByName: getFieldByNameFactory(fieldsThree), }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 4222c02388433..adb86253ab28c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -285,15 +285,10 @@ describe('loader', () => { } as unknown) as Pick, }); - expect( - cache.foo.fields.find((f: IndexPatternField) => f.name === 'bytes')!.aggregationRestrictions - ).toEqual({ + expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({ sum: { agg: 'sum' }, }); - expect( - cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')! - .aggregationRestrictions - ).toEqual({ + expect(cache.foo.getFieldByName('timestamp')!.aggregationRestrictions).toEqual({ date_histogram: { agg: 'date_histogram', fixed_interval: 'm' }, }); }); @@ -342,9 +337,7 @@ describe('loader', () => { } as unknown) as Pick, }); - expect(cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')!.meta).toEqual( - true - ); + expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 70079cce6cc46..fac5d7350e45e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -26,6 +26,7 @@ import { import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; import { documentField } from './document_field'; import { readFromStorage, writeToStorage } from '../settings_storage'; +import { getFieldByNameFactory } from './pure_helpers'; type SetState = StateSetter; type SavedObjectsClient = Pick; @@ -112,6 +113,7 @@ export async function loadIndexPatterns({ ]) ), fields: newFields, + getFieldByName: getFieldByNameFactory(newFields), hasRestrictions: !!typeMeta?.aggs, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 744a9f6743d09..2c6f42668d863 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -5,14 +5,11 @@ */ import { DragContextState } from '../drag_drop'; +import { getFieldByNameFactory } from './pure_helpers'; import { IndexPattern } from './types'; -export const createMockedIndexPattern = (): IndexPattern => ({ - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: [ +export const createMockedIndexPattern = (): IndexPattern => { + const fields = [ { name: 'timestamp', displayName: 'timestampLabel', @@ -74,16 +71,19 @@ export const createMockedIndexPattern = (): IndexPattern => ({ lang: 'painless', script: '1234', }, - ], -}); + ]; + return { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields, + getFieldByName: getFieldByNameFactory(fields), + }; +}; -export const createMockedRestrictedIndexPattern = () => ({ - id: '2', - title: 'my-fake-restricted-pattern', - timeFieldName: 'timestamp', - hasRestrictions: true, - fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, - fields: [ +export const createMockedRestrictedIndexPattern = () => { + const fields = [ { name: 'timestamp', displayName: 'timestampLabel', @@ -109,54 +109,63 @@ export const createMockedRestrictedIndexPattern = () => ({ lang: 'painless', script: '1234', }, - ], - typeMeta: { - params: { - rollup_index: 'my-fake-index-pattern', - }, - aggs: { - terms: { - source: { - agg: 'terms', - }, + ]; + return { + id: '2', + title: 'my-fake-restricted-pattern', + timeFieldName: 'timestamp', + hasRestrictions: true, + fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, + fields, + getFieldByName: getFieldByNameFactory(fields), + typeMeta: { + params: { + rollup_index: 'my-fake-index-pattern', }, - date_histogram: { - timestamp: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', + aggs: { + terms: { + source: { + agg: 'terms', + }, }, - }, - histogram: { - bytes: { - agg: 'histogram', - interval: 1000, + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, }, - }, - avg: { - bytes: { - agg: 'avg', + histogram: { + bytes: { + agg: 'histogram', + interval: 1000, + }, }, - }, - max: { - bytes: { - agg: 'max', + avg: { + bytes: { + agg: 'avg', + }, }, - }, - min: { - bytes: { - agg: 'min', + max: { + bytes: { + agg: 'max', + }, }, - }, - sum: { - bytes: { - agg: 'sum', + min: { + bytes: { + agg: 'min', + }, + }, + sum: { + bytes: { + agg: 'sum', + }, }, }, }, - }, -}); + }; +}; export function createMockedDragDropContext(): jest.Mocked { return { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 65119d3978ee6..1cfa63511a45c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -43,7 +43,7 @@ export const cardinalityOperation: OperationDefinition { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index ac6bf63c37110..fc33b64ca508f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -18,6 +18,7 @@ import { } from '../../../../../../../src/plugins/data/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; +import { getFieldByNameFactory } from '../../pure_helpers'; const dataStart = dataPluginMock.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression( @@ -66,6 +67,17 @@ describe('date_histogram', () => { searchable: true, }, ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + }, + ]), }, 2: { id: '2', @@ -81,6 +93,16 @@ describe('date_histogram', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'other_timestamp', + displayName: 'other_timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + }, + ]), }, }, layers: { @@ -267,6 +289,22 @@ describe('date_histogram', () => { }, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'timestamp', + displayName: 'timestamp', + aggregatable: true, + searchable: true, + type: 'date', + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '42w', + }, + }, + }, + ]), } ); expect(esAggsConfig).toEqual( @@ -294,7 +332,7 @@ describe('date_histogram', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newDateField = indexPattern.fields.find((i) => i.name === 'start_date')!; + const newDateField = indexPattern.getFieldByName('start_date')!; const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField); expect(column).toHaveProperty('sourceField', 'start_date'); @@ -314,7 +352,7 @@ describe('date_histogram', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newDateField = indexPattern.fields.find((i) => i.name === 'start_date')!; + const newDateField = indexPattern.getFieldByName('start_date')!; const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField); expect(column).toHaveProperty('sourceField', 'start_date'); @@ -356,6 +394,22 @@ describe('date_histogram', () => { }, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'dateField', + displayName: 'dateField', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'CET', + calendar_interval: 'w', + }, + }, + }, + ]), } ); expect(transferedColumn).toEqual( @@ -393,6 +447,15 @@ describe('date_histogram', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'dateField', + displayName: 'dateField', + type: 'date', + aggregatable: true, + searchable: true, + }, + ]), } ); expect(transferedColumn).toEqual( @@ -609,6 +672,18 @@ describe('date_histogram', () => { }, }, ], + getFieldByName: getFieldByNameFactory([ + { + ...state.indexPatterns[1].fields[0], + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '1h', + }, + }, + }, + ]), }, }, }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 185f44405bb4b..19043c03e5a61 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -81,7 +81,7 @@ export const dateHistogramOperation: OperationDefinition< }; }, isTransferable: (column, newIndexPattern) => { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && @@ -91,12 +91,9 @@ export const dateHistogramOperation: OperationDefinition< ); }, transfer: (column, newIndexPattern) => { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); - if ( - newField && - newField.aggregationRestrictions && - newField.aggregationRestrictions.date_histogram - ) { + const newField = newIndexPattern.getFieldByName(column.sourceField); + + if (newField?.aggregationRestrictions?.date_histogram) { const restrictions = newField.aggregationRestrictions.date_histogram; return { @@ -123,7 +120,7 @@ export const dateHistogramOperation: OperationDefinition< }; }, toEsAggsConfig: (column, columnId, indexPattern) => { - const usedField = indexPattern.fields.find((field) => field.name === column.sourceField); + const usedField = indexPattern.getFieldByName(column.sourceField); return { id: columnId, enabled: true, @@ -132,7 +129,7 @@ export const dateHistogramOperation: OperationDefinition< params: { field: column.sourceField, time_zone: column.params.timeZone, - useNormalizedEsInterval: !usedField || !usedField.aggregationRestrictions?.date_histogram, + useNormalizedEsInterval: !usedField?.aggregationRestrictions?.date_histogram, interval: column.params.interval, drop_partials: false, min_doc_count: 0, @@ -143,8 +140,8 @@ export const dateHistogramOperation: OperationDefinition< paramEditor: ({ state, setState, currentColumn, layerId, dateRange, data }) => { const field = currentColumn && - state.indexPatterns[state.layers[layerId].indexPatternId].fields.find( - (currentField) => currentField.name === currentColumn.sourceField + state.indexPatterns[state.layers[layerId].indexPatternId].getFieldByName( + currentColumn.sourceField ); const intervalIsRestricted = field!.aggregationRestrictions && field!.aggregationRestrictions.date_histogram; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 1d3ecc165ce74..fef575c61475c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -43,7 +43,7 @@ function buildMetricOperation>({ } }, isTransferable: (column, newIndexPattern) => { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index d43a905434c02..ce015284e544b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -30,6 +30,7 @@ import { } from './constants'; import { RangePopover } from './advanced_editor'; import { DragDropBuckets } from '../shared_components'; +import { getFieldByNameFactory } from '../../../pure_helpers'; const dataPluginMockValue = dataPluginMock.createStartContract(); // need to overwrite the formatter field first @@ -96,6 +97,9 @@ describe('ranges', () => { title: 'my_index_pattern', hasRestrictions: false, fields: [{ name: sourceField, type: 'number', displayName: sourceField }], + getFieldByName: getFieldByNameFactory([ + { name: sourceField, type: 'number', displayName: sourceField }, + ]), }, }, existingFields: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 46d9e4e6c22de..c6cc6ae13f178 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -140,7 +140,7 @@ export const rangeOperation: OperationDefinition { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && @@ -168,9 +168,7 @@ export const rangeOperation: OperationDefinition { const indexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; - const currentField = indexPattern.fields.find( - (field) => field.name === currentColumn.sourceField - ); + const currentField = indexPattern.getFieldByName(currentColumn.sourceField); const numberFormat = currentColumn.params.format; const numberFormatterPattern = numberFormat && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index dcb4646816e13..421068a5ad47f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -61,7 +61,7 @@ export const termsOperation: OperationDefinition { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 1341ca0587c75..bb1b13ba74cc5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -103,7 +103,7 @@ describe('terms', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newNumberField = indexPattern.fields.find((i) => i.name === 'bytes')!; + const newNumberField = indexPattern.getFieldByName('bytes')!; const column = termsOperation.onFieldChange(oldColumn, indexPattern, newNumberField); expect(column).toHaveProperty('dataType', 'number'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 6808bc724f26b..9767d4bdca688 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -8,38 +8,42 @@ import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColum import { AvgIndexPatternColumn } from './definitions/metrics'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; +import { getFieldByNameFactory } from '../pure_helpers'; jest.mock('../loader'); +const fields = [ + { + name: 'timestamp', + displayName: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + const expectedIndexPatterns = { 1: { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestamp', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts index 9e81b5e0c5bf9..c5da3d0c5dcde 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternPrivateState } from './types'; +import { keyBy } from 'lodash'; +import { IndexPatternField, IndexPatternPrivateState } from './types'; export function fieldExists( existingFields: IndexPatternPrivateState['existingFields'], @@ -13,3 +14,8 @@ export function fieldExists( ) { return existingFields[indexPatternTitle] && existingFields[indexPatternTitle][fieldName]; } + +export function getFieldByNameFactory(newFields: IndexPatternField[]) { + const fieldsLookup = keyBy(newFields, 'name'); + return (name: string) => fieldsLookup[name]; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index da90a2ce5fcec..45008b2d9439a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -16,6 +16,7 @@ import { TermsIndexPatternColumn } from './operations/definitions/terms'; import { DateHistogramIndexPatternColumn } from './operations/definitions/date_histogram'; import { AvgIndexPatternColumn } from './operations/definitions/metrics'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./operations'); @@ -585,59 +586,61 @@ describe('state_helpers', () => { }); describe('updateLayerIndexPattern', () => { - const indexPattern: IndexPattern = { - id: 'test', - title: '', - hasRestrictions: true, - fields: [ - { - name: 'fieldA', - displayName: 'fieldA', - aggregatable: true, - searchable: true, - type: 'string', - }, - { - name: 'fieldB', - displayName: 'fieldB', - aggregatable: true, - searchable: true, - type: 'number', - aggregationRestrictions: { - avg: { - agg: 'avg', - }, + const fields = [ + { + name: 'fieldA', + displayName: 'fieldA', + aggregatable: true, + searchable: true, + type: 'string', + }, + { + name: 'fieldB', + displayName: 'fieldB', + aggregatable: true, + searchable: true, + type: 'number', + aggregationRestrictions: { + avg: { + agg: 'avg', }, }, - { - name: 'fieldC', - displayName: 'fieldC', - aggregatable: false, - searchable: true, - type: 'date', - }, - { - name: 'fieldD', - displayName: 'fieldD', - aggregatable: true, - searchable: true, - type: 'date', - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - time_zone: 'CET', - calendar_interval: 'w', - }, + }, + { + name: 'fieldC', + displayName: 'fieldC', + aggregatable: false, + searchable: true, + type: 'date', + }, + { + name: 'fieldD', + displayName: 'fieldD', + aggregatable: true, + searchable: true, + type: 'date', + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'CET', + calendar_interval: 'w', }, }, - { - name: 'fieldE', - displayName: 'fieldE', - aggregatable: true, - searchable: true, - type: 'date', - }, - ], + }, + { + name: 'fieldE', + displayName: 'fieldE', + aggregatable: true, + searchable: true, + type: 'date', + }, + ]; + const indexPattern: IndexPattern = { + id: 'test', + title: '', + hasRestrictions: true, + getFieldByName: getFieldByNameFactory(fields), + fields, }; it('should switch index pattern id in layer', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index a3c0e8aed7421..1e6fc5a5806b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -11,6 +11,7 @@ import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/pub export interface IndexPattern { id: string; fields: IndexPatternField[]; + getFieldByName(name: string): IndexPatternField | undefined; title: string; timeFieldName?: string; fieldFormatMap?: Record< diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index d3d65617f2253..d0ea81d135156 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -87,15 +87,15 @@ export function fieldIsInvalid( indexPattern: IndexPattern ) { const operationDefinition = operationType && operationDefinitionMap[operationType]; + const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined; return Boolean( sourceField && operationDefinition && - !indexPattern.fields.some( - (field) => - field.name === sourceField && - operationDefinition?.input === 'field' && - operationDefinition.getPossibleOperationForField(field) !== undefined + !( + field && + operationDefinition?.input === 'field' && + operationDefinition.getPossibleOperationForField(field) !== undefined ) ); } From dc489e48a4942a43a860f44d8a21f54aad210fc4 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Tue, 10 Nov 2020 14:14:25 +0000 Subject: [PATCH 09/19] [Discover] Add metric on adding filter (#82961) --- src/plugins/discover/public/application/angular/discover.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 389eda90e00a1..af763240bccfd 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -65,6 +65,7 @@ const { timefilter, toastNotifications, uiSettings: config, + trackUiMetric, } = getServices(); import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; @@ -81,6 +82,7 @@ import { DOC_HIDE_TIME_COLUMN_SETTING, MODIFY_COLUMNS_ON_SWITCH, } from '../../../common'; +import { METRIC_TYPE } from '@kbn/analytics'; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -990,6 +992,9 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise operation, $scope.indexPattern.id ); + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); + } return filterManager.addFilters(newFilters); }; From b2d6b66fe505b5af504073d448ed4b84785fdb39 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 10 Nov 2020 17:15:26 +0300 Subject: [PATCH 10/19] [bundle optimization] fix imports of react-use lib (#82847) * [bundle optimization] fix imports of react-use lib * add 2 more files * add rule into eslintrc.js Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .eslintrc.js | 4 ++++ .../home/public/application/components/home_app.js | 2 +- .../public/components/controls/date_ranges.tsx | 2 +- .../public/components/controls/field.tsx | 2 +- .../public/components/controls/filters.tsx | 2 +- .../public/components/controls/order_by.tsx | 2 +- .../public/components/controls/radius_ratio_option.tsx | 2 +- .../public/components/controls/sub_metric.tsx | 2 +- .../public/components/sidebar/controls.tsx | 2 +- .../application/components/visualize_listing.tsx | 4 +++- .../URLFilter/URLSearch/SelectableUrlList.tsx | 2 +- .../components/app/ServiceMap/useRefDimensions.ts | 2 +- .../plugins/fleet/public/applications/fleet/index.tsx | 2 +- .../expression_editor/criterion_preview_chart.tsx | 2 +- .../components/expression_editor/editor.tsx | 2 +- .../infra/public/components/log_stream/index.tsx | 2 +- .../setup_flyout/log_entry_categories_setup_view.tsx | 2 +- .../logs/log_analysis/log_analysis_setup_state.ts | 2 +- .../use_log_entry_categories_quality.ts | 2 +- .../infra/public/containers/logs/log_entries/index.ts | 2 +- .../containers/logs/log_highlights/log_highlights.tsx | 2 +- .../containers/logs/log_position/log_position_state.ts | 2 +- .../public/containers/logs/log_source/log_source.ts | 2 +- .../public/containers/logs/log_summary/with_summary.ts | 2 +- x-pack/plugins/infra/public/hooks/use_kibana_space.ts | 2 +- .../infra/public/hooks/use_kibana_timefilter_time.tsx | 5 ++++- .../public/pages/link_to/redirect_to_node_logs.tsx | 2 +- .../sections/top_categories/top_categories_table.tsx | 2 +- .../log_entry_rate/sections/anomalies/expanded_row.tsx | 2 +- .../logs/log_entry_rate/sections/anomalies/table.tsx | 2 +- .../log_entry_rate/use_log_entry_anomalies_results.ts | 2 +- .../plugins/infra/public/pages/logs/page_content.tsx | 2 +- .../pages/metrics/inventory_view/components/layout.tsx | 2 +- .../plugins/infra/public/utils/use_tracked_promise.ts | 2 +- .../operations/definitions/filters/filter_popover.tsx | 2 +- .../operations/definitions/ranges/advanced_editor.tsx | 2 +- .../operations/definitions/ranges/range_editor.tsx | 2 +- .../definitions/shared_components/label_input.tsx | 2 +- .../definitions/terms/values_range_input.test.tsx | 4 +--- .../definitions/terms/values_range_input.tsx | 2 +- .../plugins/lens/public/pie_visualization/toolbar.tsx | 2 +- .../logstash/public/application/pipeline_edit_view.tsx | 2 +- .../public/common/mock/endpoint/app_root_provider.tsx | 2 +- .../components/aggregation_list/popover_form.tsx | 2 +- .../common/filter_agg/components/filter_agg_form.tsx | 2 +- .../common/filter_agg/components/filter_term_form.tsx | 2 +- .../__tests__/step_screenshot_display.test.tsx | 10 ++++------ .../monitor/synthetics/step_screenshot_display.tsx | 2 +- 48 files changed, 59 insertions(+), 54 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5c2a2817eae53..561e9bc55bf9d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -640,6 +640,10 @@ module.exports = { name: 'lodash/fp/assocPath', message: 'Please use @elastic/safer-lodash-set instead', }, + { + name: 'react-use', + message: 'Please use react-use/lib/{method} instead.', + }, ], }, ], diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 7fe4f4351c35b..734100fe584ab 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -28,7 +28,7 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { getTutorial } from '../load_tutorials'; import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../kibana_services'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; const RedirectToDefaultApp = () => { useMount(() => { diff --git a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx index 785ef1b83a23d..90ee1dd4f02ae 100644 --- a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -36,7 +36,7 @@ import dateMath from '@elastic/datemath'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { isEqual, omit } from 'lodash'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { DocLinksStart } from 'src/core/public'; import { useKibana } from '../../../../kibana_react/public'; diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx index 9529adfe12720..cb6e9d9aa7ba8 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -19,7 +19,7 @@ import { get } from 'lodash'; import React, { useState, useCallback } from 'react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/src/plugins/vis_default_editor/public/components/controls/filters.tsx b/src/plugins/vis_default_editor/public/components/controls/filters.tsx index b2e6373edfc10..4c5181ab316d1 100644 --- a/src/plugins/vis_default_editor/public/components/controls/filters.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filters.tsx @@ -21,7 +21,7 @@ import React, { useState, useEffect } from 'react'; import { omit, isEqual } from 'lodash'; import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { Query, DataPublicPluginStart } from '../../../../data/public'; import { IUiSettingsClient } from '../../../../../core/public'; diff --git a/src/plugins/vis_default_editor/public/components/controls/order_by.tsx b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx index 16aeafaab253b..000719318f107 100644 --- a/src/plugins/vis_default_editor/public/components/controls/order_by.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { isCompatibleAggregation, diff --git a/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx index c2c21e7c1a058..00e6b6da88b05 100644 --- a/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx @@ -21,7 +21,7 @@ import React, { useCallback } from 'react'; import { EuiFormRow, EuiIconTip, EuiRange, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { AggControlProps } from './agg_control_props'; diff --git a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx index fc79ba703c2b4..4b0637edf4055 100644 --- a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFormLabel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; diff --git a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx index df9818b237962..388ac39f06475 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx @@ -21,7 +21,7 @@ import React, { useCallback, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { Vis } from 'src/plugins/visualizations/public'; import { discardChanges, EditorAction } from './state'; diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 2edabbf46f9d8..718bd2ed343ce 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -21,7 +21,9 @@ import './visualize_listing.scss'; import React, { useCallback, useRef, useMemo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { useUnmount, useMount } from 'react-use'; +import useUnmount from 'react-use/lib/useUnmount'; +import useMount from 'react-use/lib/useMount'; + import { useLocation } from 'react-router-dom'; import { SavedObjectsFindOptionsReference } from '../../../../../core/public'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx index 7bd9b2c87814b..1d314e3a0e0d3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -31,7 +31,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { useEvent } from 'react-use'; +import useEvent from 'react-use/lib/useEvent'; import { formatOptions, selectableRenderOptions, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts index c8639b334f66a..fc478e27ccac3 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { useRef } from 'react'; -import { useWindowSize } from 'react-use'; +import useWindowSize from 'react-use/lib/useWindowSize'; export function useRefDimensions() { const ref = useRef(null); diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index 03cc4a8b7aff7..a49306f8e8d55 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -5,7 +5,7 @@ */ import React, { memo, useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { HashRouter as Router, Redirect, Switch, Route, RouteProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 478d4b879a7e1..1d23b4d4778ca 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -5,7 +5,7 @@ */ import React, { useMemo } from 'react'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { ScaleType, AnnotationDomainTypes, diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index c5b0ed5844852..48639f3095d3d 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression'; import { ForLastExpression, diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index f9bfbf9564798..6698018e8cc19 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; import { noop } from 'lodash'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { euiStyled } from '../../../../observability/public'; import { LogEntriesCursor } from '../../../common/http_api'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/log_entry_categories_setup_view.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/log_entry_categories_setup_view.tsx index e7961a11a4d52..5ff15012ad245 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/log_entry_categories_setup_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/log_entry_categories_setup_view.tsx @@ -6,7 +6,7 @@ import { EuiSpacer, EuiSteps, EuiText, EuiTitle } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { useLogEntryCategoriesSetup } from '../../../../containers/logs/log_analysis/modules/log_entry_categories'; import { createInitialConfigurationStep } from '../initial_configuration_step'; import { createProcessStep } from '../process_step'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts index 750a7104a3a98..821d0373d316a 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts @@ -6,7 +6,7 @@ import { isEqual } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { usePrevious } from 'react-use'; +import usePrevious from 'react-use/lib/usePrevious'; import { combineDatasetFilters, DatasetFilter, diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/use_log_entry_categories_quality.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/use_log_entry_categories_quality.ts index 6bad94ec49f87..c490f7d74fc9d 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/use_log_entry_categories_quality.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/modules/log_entry_categories/use_log_entry_categories_quality.ts @@ -5,7 +5,7 @@ */ import { useMemo, useState } from 'react'; -import { useDeepCompareEffect } from 'react-use'; +import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; import { CategoryQualityWarningReason, QualityWarning, diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts index 214bb16b24283..146746af980c9 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { useEffect, useState, useReducer, useCallback } from 'react'; -import { useMountedState } from 'react-use'; +import useMountedState from 'react-use/lib/useMountedState'; import createContainer from 'constate'; import { pick, throttle } from 'lodash'; import { TimeKey, timeKeyIsBetween } from '../../../../common/time'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx b/x-pack/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx index 941e89848131b..3081e37da1d37 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx @@ -6,7 +6,7 @@ import createContainer from 'constate'; import { useState, useContext } from 'react'; -import { useThrottle } from 'react-use'; +import useThrottle from 'react-use/lib/useThrottle'; import { useLogEntryHighlights } from './log_entry_highlights'; import { useLogSummaryHighlights } from './log_summary_highlights'; import { useNextAndPrevious } from './next_and_previous'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index 4b110fbc4e51e..f1aed0fa75165 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -6,7 +6,7 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; import createContainer from 'constate'; -import { useSetState } from 'react-use'; +import useSetState from 'react-use/lib/useSetState'; import { TimeKey } from '../../../../common/time'; import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; import { useKibanaTimefilterTime } from '../../../hooks/use_kibana_timefilter_time'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index e2dd4c523c03f..75c328b829397 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -6,7 +6,7 @@ import createContainer from 'constate'; import { useCallback, useMemo, useState } from 'react'; -import { useMountedState } from 'react-use'; +import useMountedState from 'react-use/lib/useMountedState'; import type { HttpHandler } from 'src/core/public'; import { LogSourceConfiguration, diff --git a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts index 625a1ead4d930..1a420d7dddf91 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts @@ -5,7 +5,7 @@ */ import { useContext } from 'react'; -import { useThrottle } from 'react-use'; +import useThrottle from 'react-use/lib/useThrottle'; import { RendererFunction } from '../../../utils/typed_react'; import { LogSummaryBuckets, useLogSummary } from './log_summary'; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_space.ts b/x-pack/plugins/infra/public/hooks/use_kibana_space.ts index 1c06263008961..39caa4f063cd5 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana_space.ts +++ b/x-pack/plugins/infra/public/hooks/use_kibana_space.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useAsync } from 'react-use'; +import useAsync from 'react-use/lib/useAsync'; import { useKibanaContextForPlugin } from '../hooks/use_kibana'; import type { Space } from '../../../spaces/public'; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx index e9537370f12cd..193a285c6b882 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx +++ b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx @@ -5,7 +5,10 @@ */ import { useCallback } from 'react'; -import { useUpdateEffect, useMount } from 'react-use'; + +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; +import useMount from 'react-use/lib/useMount'; + import { useKibanaContextForPlugin } from './use_kibana'; import { TimeRange, TimefilterContract } from '../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 0541b811508ee..6205d9883e535 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import flowRight from 'lodash/flowRight'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { HttpStart } from 'src/core/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { findInventoryFields } from '../../../common/inventory_models'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx index 4fd8aa3cdc1dd..ab4195492a706 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx @@ -8,7 +8,7 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { useSet } from 'react-use'; +import useSet from 'react-use/lib/useSet'; import { euiStyled } from '../../../../../../../observability/public'; import { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index 84ef13cc70706..9c78c421af752 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiTitle } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { euiStyled } from '../../../../../../../observability/public'; import { LogEntryAnomaly } from '../../../../../../common/http_api'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index e0a3b6fb91db0..855113d66f510 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -17,7 +17,7 @@ import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; -import { useSet } from 'react-use'; +import useSet from 'react-use/lib/useSet'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { formatAnomalyScore, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts index 37c99272f0872..e626e7477f2c0 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts @@ -5,7 +5,7 @@ */ import { useMemo, useState, useCallback, useEffect, useReducer } from 'react'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { useTrackedPromise, CanceledPromiseError } from '../../../utils/use_tracked_promise'; import { callGetLogEntryAnomaliesAPI } from './service_calls/get_log_entry_anomalies'; import { callGetLogEntryAnomaliesDatasetsAPI } from './service_calls/get_log_entry_anomalies_datasets'; diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index e85f0d9bf446b..10189c0d8076c 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { AlertDropdown } from '../../alerting/log_threshold'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index b9caef704d071..f6e9d45c4d225 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback, useEffect } from 'react'; -import { useInterval } from 'react-use'; +import useInterval from 'react-use/lib/useInterval'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { AutoSizer } from '../../../../components/auto_sizer'; diff --git a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts index 42518127f68bf..75c9d1bacc675 100644 --- a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts +++ b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts @@ -7,7 +7,7 @@ /* eslint-disable max-classes-per-file */ import { DependencyList, useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { useMountedState } from 'react-use'; +import useMountedState from 'react-use/lib/useMountedState'; interface UseTrackedPromiseArgs { createPromise: (...args: Arguments) => Promise; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index 077e07a89f788..b023a9a5a3ec5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -6,7 +6,7 @@ import './filter_popover.scss'; import React, { MouseEventHandler, useState } from 'react'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { EuiPopover, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FilterValue, defaultLabel, isQueryValid } from '.'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index c6773e806aef8..2eb971aa03c55 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -8,7 +8,7 @@ import './advanced_editor.scss'; import React, { useState, MouseEventHandler } from 'react'; import { i18n } from '@kbn/i18n'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { EuiFlexGroup, EuiFlexItem, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx index 8ed17a813e7fd..a18c47f9dedd1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { EuiButtonEmpty, EuiFormRow, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx index 09a98d9e48ef7..f0ee30bb4331b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx @@ -5,7 +5,7 @@ */ import React, { useState, useEffect } from 'react'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { EuiFieldText, keys } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx index c1620dd316a60..18b9b5b1e8b98 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx @@ -10,9 +10,7 @@ import { shallow } from 'enzyme'; import { EuiRange } from '@elastic/eui'; import { ValuesRangeInput } from './values_range_input'; -jest.mock('react-use', () => ({ - useDebounce: (fn: () => void) => fn(), -})); +jest.mock('react-use/lib/useDebounce', () => (fn: () => void) => fn()); describe('ValuesRangeInput', () => { it('should render EuiRange correctly', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx index 6bfde4b652571..ef42f2d4a7175 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx @@ -5,7 +5,7 @@ */ import React, { useState } from 'react'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { EuiRange } from '@elastic/eui'; diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index d69164de8a6aa..f1eb4ccdd9cf7 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -6,7 +6,7 @@ import './toolbar.scss'; import React, { useState } from 'react'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, diff --git a/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx b/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx index 9b291e5261033..a36ef394f3327 100644 --- a/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx +++ b/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx @@ -5,7 +5,7 @@ */ import React, { useState, useLayoutEffect, useCallback } from 'react'; -import { usePromise } from 'react-use'; +import usePromise from 'react-use/lib/usePromise'; import { History } from 'history'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx index 3b7262e8a8d7e..fd6a483e538b8 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx @@ -9,7 +9,7 @@ import { Provider } from 'react-redux'; import { I18nProvider } from '@kbn/i18n/react'; import { Router } from 'react-router-dom'; import { History } from 'history'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { Store } from 'redux'; import { EuiThemeProvider } from '../../../../../xpack_legacy/common'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 30e8c2b594db7..dbdff6c6b311e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { cloneDeep } from 'lodash'; -import { useUpdateEffect } from 'react-use'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { AggName } from '../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../common/types/common'; import { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx index ac6e93d3ed5eb..4e45ec7689235 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx @@ -7,7 +7,7 @@ import React, { useContext, useMemo } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useUpdateEffect } from 'react-use'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; import { commonFilterAggs, filterAggsFieldSupport } from '../constants'; import { IndexPattern } from '../../../../../../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index d59f99192621c..f9f345610e1eb 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { EuiComboBox, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; -import { useUpdateEffect } from 'react-use'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { i18n } from '@kbn/i18n'; import { isEsSearchResponse } from '../../../../../../../../../common/api_schemas/type_guards'; import { useApi } from '../../../../../../../hooks'; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/step_screenshot_display.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/step_screenshot_display.test.tsx index 16db430dbd73a..9d94514aa48c5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/step_screenshot_display.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/step_screenshot_display.test.tsx @@ -6,15 +6,13 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; -import * as reactUse from 'react-use'; import { StepScreenshotDisplay } from '../step_screenshot_display'; -describe('StepScreenshotDisplayProps', () => { - // @ts-ignore missing fields don't matter in this test, the component in question only relies on `isIntersecting` - jest.spyOn(reactUse, 'useIntersection').mockImplementation(() => ({ - isIntersecting: true, - })); +jest.mock('react-use/lib/useIntersection', () => () => ({ + isIntersecting: true, +})); +describe('StepScreenshotDisplayProps', () => { it('displays screenshot thumbnail when present', () => { const wrapper = mountWithIntl( Date: Tue, 10 Nov 2020 15:16:58 +0100 Subject: [PATCH 11/19] migrate i18n mixin to KP (#81799) * migrate i18n mixin to KP * directly load the config from i18n service * add base tests * update telemetry schema * update legacy telemetry schema * fix server tests * use paths from config service instead of manually loading the plugin config * add tests for get_translation_paths * add tests for i18n service * add plugin service test * update generated doc * improve tsdoc --- ...ibana-plugin-core-server.coresetup.i18n.md | 13 +++ .../kibana-plugin-core-server.coresetup.md | 1 + ...-core-server.i18nservicesetup.getlocale.md | 17 ++++ ...er.i18nservicesetup.gettranslationfiles.md | 17 ++++ ...ana-plugin-core-server.i18nservicesetup.md | 20 +++++ .../core/server/kibana-plugin-core-server.md | 1 + .../environment/environment_service.mock.ts | 3 +- .../constants.ts => core/server/i18n/fs.ts} | 5 +- .../i18n/get_kibana_translation_files.test.ts | 81 +++++++++++++++++ .../i18n/get_kibana_translation_files.ts} | 29 +++--- .../i18n/get_translation_paths.test.mocks.ts | 26 ++++++ .../server/i18n/get_translation_paths.test.ts | 90 +++++++++++++++++++ .../server/i18n/get_translation_paths.ts | 12 +-- src/core/server/i18n/i18n_config.ts | 29 ++++++ src/core/server/i18n/i18n_service.mock.ts | 50 +++++++++++ .../server/i18n/i18n_service.test.mocks.ts | 28 ++++++ src/core/server/i18n/i18n_service.test.ts | 84 +++++++++++++++++ src/core/server/i18n/i18n_service.ts | 75 ++++++++++++++++ src/{legacy => core}/server/i18n/index.ts | 3 +- src/core/server/i18n/init_translations.ts | 31 +++++++ src/core/server/index.ts | 5 ++ src/core/server/internal_types.ts | 2 + src/core/server/legacy/legacy_service.test.ts | 2 + src/core/server/legacy/legacy_service.ts | 1 + .../server/metrics/metrics_service.mock.ts | 2 +- src/core/server/mocks.ts | 4 + src/core/server/plugins/plugin_context.ts | 1 + .../server/plugins/plugins_service.mock.ts | 2 +- .../server/plugins/plugins_service.test.ts | 18 ++++ src/core/server/plugins/plugins_service.ts | 1 + .../server/plugins/plugins_system.test.ts | 9 ++ src/core/server/plugins/plugins_system.ts | 4 + .../rendering/__mocks__/rendering_service.ts | 4 +- .../saved_objects_service.mock.ts | 3 +- src/core/server/server.api.md | 8 ++ src/core/server/server.test.mocks.ts | 6 ++ src/core/server/server.test.ts | 7 ++ src/core/server/server.ts | 15 +++- src/core/server/status/status_service.mock.ts | 2 +- .../ui_settings/ui_settings_service.mock.ts | 2 +- src/legacy/server/config/schema.js | 13 +-- .../i18n/get_kibana_translation_paths.test.ts | 58 ------------ src/legacy/server/i18n/i18n_mixin.ts | 63 ------------- src/legacy/server/kbn_server.js | 4 - .../server/__snapshots__/index.test.ts.snap | 2 + .../server/collectors/index.ts | 1 + .../localization/file_integrity.test.mocks.ts | 0 .../localization/file_integrity.test.ts | 0 .../localization/file_integrity.ts | 0 .../server/collectors}/localization/index.ts | 0 .../telemetry_localization_collector.test.ts | 0 .../telemetry_localization_collector.ts | 18 ++-- .../kibana_usage_collection/server/plugin.ts | 2 + .../telemetry/schema/legacy_plugins.json | 20 +---- src/plugins/telemetry/schema/oss_plugins.json | 17 ++++ 55 files changed, 709 insertions(+), 202 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md rename src/{legacy/server/i18n/constants.ts => core/server/i18n/fs.ts} (88%) create mode 100644 src/core/server/i18n/get_kibana_translation_files.test.ts rename src/{legacy/server/i18n/get_kibana_translation_paths.ts => core/server/i18n/get_kibana_translation_files.ts} (64%) create mode 100644 src/core/server/i18n/get_translation_paths.test.mocks.ts create mode 100644 src/core/server/i18n/get_translation_paths.test.ts rename src/{legacy => core}/server/i18n/get_translation_paths.ts (85%) create mode 100644 src/core/server/i18n/i18n_config.ts create mode 100644 src/core/server/i18n/i18n_service.mock.ts create mode 100644 src/core/server/i18n/i18n_service.test.mocks.ts create mode 100644 src/core/server/i18n/i18n_service.test.ts create mode 100644 src/core/server/i18n/i18n_service.ts rename src/{legacy => core}/server/i18n/index.ts (86%) create mode 100644 src/core/server/i18n/init_translations.ts delete mode 100644 src/legacy/server/i18n/get_kibana_translation_paths.test.ts delete mode 100644 src/legacy/server/i18n/i18n_mixin.ts rename src/{legacy/server/i18n => plugins/kibana_usage_collection/server/collectors}/localization/file_integrity.test.mocks.ts (100%) rename src/{legacy/server/i18n => plugins/kibana_usage_collection/server/collectors}/localization/file_integrity.test.ts (100%) rename src/{legacy/server/i18n => plugins/kibana_usage_collection/server/collectors}/localization/file_integrity.ts (100%) rename src/{legacy/server/i18n => plugins/kibana_usage_collection/server/collectors}/localization/index.ts (100%) rename src/{legacy/server/i18n => plugins/kibana_usage_collection/server/collectors}/localization/telemetry_localization_collector.test.ts (100%) rename src/{legacy/server/i18n => plugins/kibana_usage_collection/server/collectors}/localization/telemetry_localization_collector.ts (82%) diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md new file mode 100644 index 0000000000000..cac878c1e4449 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [i18n](./kibana-plugin-core-server.coresetup.i18n.md) + +## CoreSetup.i18n property + +[I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) + +Signature: + +```typescript +i18n: I18nServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 7a733cc34dace..1171dbad570ce 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -21,6 +21,7 @@ export interface CoreSetupElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | +| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | I18nServiceSetup | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | | [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md new file mode 100644 index 0000000000000..2fe8e564e7ce5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) > [getLocale](./kibana-plugin-core-server.i18nservicesetup.getlocale.md) + +## I18nServiceSetup.getLocale() method + +Return the locale currently in use. + +Signature: + +```typescript +getLocale(): string; +``` +Returns: + +`string` + diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md new file mode 100644 index 0000000000000..81caed287454e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) > [getTranslationFiles](./kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md) + +## I18nServiceSetup.getTranslationFiles() method + +Return the absolute paths to translation files currently in use. + +Signature: + +```typescript +getTranslationFiles(): string[]; +``` +Returns: + +`string[]` + diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md new file mode 100644 index 0000000000000..f68b7877953e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) + +## I18nServiceSetup interface + + +Signature: + +```typescript +export interface I18nServiceSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [getLocale()](./kibana-plugin-core-server.i18nservicesetup.getlocale.md) | Return the locale currently in use. | +| [getTranslationFiles()](./kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md) | Return the absolute paths to translation files currently in use. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 68f5e72915556..adbb2460dc80a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -89,6 +89,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | +| [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | | [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. | diff --git a/src/core/server/environment/environment_service.mock.ts b/src/core/server/environment/environment_service.mock.ts index a956e369ba4a7..3c579b0f68b00 100644 --- a/src/core/server/environment/environment_service.mock.ts +++ b/src/core/server/environment/environment_service.mock.ts @@ -17,8 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; - -import { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; +import type { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { diff --git a/src/legacy/server/i18n/constants.ts b/src/core/server/i18n/fs.ts similarity index 88% rename from src/legacy/server/i18n/constants.ts rename to src/core/server/i18n/fs.ts index a7a410dbcb5b3..23d729504f81c 100644 --- a/src/legacy/server/i18n/constants.ts +++ b/src/core/server/i18n/fs.ts @@ -17,4 +17,7 @@ * under the License. */ -export const I18N_RC = '.i18nrc.json'; +import Fs from 'fs'; +import { promisify } from 'util'; + +export const readFile = promisify(Fs.readFile); diff --git a/src/core/server/i18n/get_kibana_translation_files.test.ts b/src/core/server/i18n/get_kibana_translation_files.test.ts new file mode 100644 index 0000000000000..737d8ed8bc4e2 --- /dev/null +++ b/src/core/server/i18n/get_kibana_translation_files.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { getKibanaTranslationFiles } from './get_kibana_translation_files'; +import { getTranslationPaths } from './get_translation_paths'; + +const mockGetTranslationPaths = getTranslationPaths as jest.Mock; + +jest.mock('./get_translation_paths', () => ({ + getTranslationPaths: jest.fn().mockResolvedValue([]), +})); +jest.mock('../utils', () => ({ + fromRoot: jest.fn().mockImplementation((path: string) => path), +})); + +const locale = 'en'; + +describe('getKibanaTranslationPaths', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls getTranslationPaths against kibana root and kibana-extra', async () => { + await getKibanaTranslationFiles(locale, []); + + expect(mockGetTranslationPaths).toHaveBeenCalledTimes(2); + + expect(mockGetTranslationPaths).toHaveBeenCalledWith({ + cwd: '.', + nested: true, + }); + + expect(mockGetTranslationPaths).toHaveBeenCalledWith({ + cwd: '../kibana-extra', + nested: true, + }); + }); + + it('calls getTranslationPaths for each config returned in plugin.paths and plugins.scanDirs', async () => { + const pluginPaths = ['/path/to/pluginA', '/path/to/pluginB']; + + await getKibanaTranslationFiles(locale, pluginPaths); + + expect(mockGetTranslationPaths).toHaveBeenCalledTimes(2 + pluginPaths.length); + + pluginPaths.forEach((pluginPath) => { + expect(mockGetTranslationPaths).toHaveBeenCalledWith({ + cwd: pluginPath, + nested: false, + }); + }); + }); + + it('only return files for specified locale', async () => { + mockGetTranslationPaths.mockResolvedValueOnce(['/root/en.json', '/root/fr.json']); + mockGetTranslationPaths.mockResolvedValueOnce([ + '/kibana-extra/en.json', + '/kibana-extra/fr.json', + ]); + + const translationFiles = await getKibanaTranslationFiles('en', []); + + expect(translationFiles).toEqual(['/root/en.json', '/kibana-extra/en.json']); + }); +}); diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.ts b/src/core/server/i18n/get_kibana_translation_files.ts similarity index 64% rename from src/legacy/server/i18n/get_kibana_translation_paths.ts rename to src/core/server/i18n/get_kibana_translation_files.ts index d7f77d3185ba4..dacb6a1e16a5c 100644 --- a/src/legacy/server/i18n/get_kibana_translation_paths.ts +++ b/src/core/server/i18n/get_kibana_translation_files.ts @@ -17,26 +17,27 @@ * under the License. */ -import { KibanaConfig } from '../kbn_server'; -import { fromRoot } from '../../../core/server/utils'; -import { I18N_RC } from './constants'; +import { basename } from 'path'; +import { fromRoot } from '../utils'; import { getTranslationPaths } from './get_translation_paths'; -export async function getKibanaTranslationPaths(config: Pick) { - return await Promise.all([ +export const getKibanaTranslationFiles = async ( + locale: string, + pluginPaths: string[] +): Promise => { + const translationPaths = await Promise.all([ getTranslationPaths({ cwd: fromRoot('.'), - glob: `*/${I18N_RC}`, + nested: true, }), - ...(config.get('plugins.paths') as string[]).map((cwd) => - getTranslationPaths({ cwd, glob: I18N_RC }) - ), - ...(config.get('plugins.scanDirs') as string[]).map((cwd) => - getTranslationPaths({ cwd, glob: `*/${I18N_RC}` }) - ), + ...pluginPaths.map((pluginPath) => getTranslationPaths({ cwd: pluginPath, nested: false })), getTranslationPaths({ cwd: fromRoot('../kibana-extra'), - glob: `*/${I18N_RC}`, + nested: true, }), ]); -} + + return ([] as string[]) + .concat(...translationPaths) + .filter((translationPath) => basename(translationPath, '.json') === locale); +}; diff --git a/src/core/server/i18n/get_translation_paths.test.mocks.ts b/src/core/server/i18n/get_translation_paths.test.mocks.ts new file mode 100644 index 0000000000000..f3b688062523c --- /dev/null +++ b/src/core/server/i18n/get_translation_paths.test.mocks.ts @@ -0,0 +1,26 @@ +/* + * 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 globbyMock = jest.fn(); +jest.doMock('globby', () => globbyMock); + +export const readFileMock = jest.fn(); +jest.doMock('./fs', () => ({ + readFile: readFileMock, +})); diff --git a/src/core/server/i18n/get_translation_paths.test.ts b/src/core/server/i18n/get_translation_paths.test.ts new file mode 100644 index 0000000000000..c6af59da07fb5 --- /dev/null +++ b/src/core/server/i18n/get_translation_paths.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { resolve, join } from 'path'; +import { globbyMock, readFileMock } from './get_translation_paths.test.mocks'; +import { getTranslationPaths } from './get_translation_paths'; + +describe('getTranslationPaths', () => { + beforeEach(() => { + globbyMock.mockReset(); + readFileMock.mockReset(); + + globbyMock.mockResolvedValue([]); + readFileMock.mockResolvedValue('{}'); + }); + + it('calls `globby` with the correct parameters', async () => { + getTranslationPaths({ cwd: '/some/cwd', nested: false }); + + expect(globbyMock).toHaveBeenCalledTimes(1); + expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd' }); + + globbyMock.mockClear(); + + await getTranslationPaths({ cwd: '/other/cwd', nested: true }); + + expect(globbyMock).toHaveBeenCalledTimes(1); + expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd' }); + }); + + it('calls `readFile` for each entry returned by `globby`', async () => { + const entries = [join('pathA', '.i18nrc.json'), join('pathB', '.i18nrc.json')]; + globbyMock.mockResolvedValue(entries); + + const cwd = '/kibana-extra'; + + await getTranslationPaths({ cwd, nested: true }); + + expect(readFileMock).toHaveBeenCalledTimes(2); + + expect(readFileMock).toHaveBeenNthCalledWith(1, resolve(cwd, entries[0]), 'utf8'); + expect(readFileMock).toHaveBeenNthCalledWith(2, resolve(cwd, entries[1]), 'utf8'); + }); + + it('returns the absolute path to the translation files', async () => { + const entries = ['.i18nrc.json']; + globbyMock.mockResolvedValue(entries); + + const i18nFileContent = { + translations: ['translations/en.json', 'translations/fr.json'], + }; + readFileMock.mockResolvedValue(JSON.stringify(i18nFileContent)); + + const cwd = '/cwd'; + + const translationPaths = await getTranslationPaths({ cwd, nested: true }); + + expect(translationPaths).toEqual([ + resolve(cwd, 'translations/en.json'), + resolve(cwd, 'translations/fr.json'), + ]); + }); + + it('throws if i18nrc parsing fails', async () => { + globbyMock.mockResolvedValue(['.i18nrc.json']); + readFileMock.mockRejectedValue(new Error('error parsing file')); + + await expect( + getTranslationPaths({ cwd: '/cwd', nested: true }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to parse .i18nrc.json file at /cwd/.i18nrc.json"` + ); + }); +}); diff --git a/src/legacy/server/i18n/get_translation_paths.ts b/src/core/server/i18n/get_translation_paths.ts similarity index 85% rename from src/legacy/server/i18n/get_translation_paths.ts rename to src/core/server/i18n/get_translation_paths.ts index a2a292e2278be..41d9dc4722e37 100644 --- a/src/legacy/server/i18n/get_translation_paths.ts +++ b/src/core/server/i18n/get_translation_paths.ts @@ -17,18 +17,18 @@ * under the License. */ -import { promisify } from 'util'; -import { readFile } from 'fs'; import { resolve, dirname } from 'path'; import globby from 'globby'; - -const readFileAsync = promisify(readFile); +import { readFile } from './fs'; interface I18NRCFileStructure { translations?: string[]; } -export async function getTranslationPaths({ cwd, glob }: { cwd: string; glob: string }) { +const I18N_RC = '.i18nrc.json'; + +export async function getTranslationPaths({ cwd, nested }: { cwd: string; nested: boolean }) { + const glob = nested ? `*/${I18N_RC}` : I18N_RC; const entries = await globby(glob, { cwd }); const translationPaths: string[] = []; @@ -36,7 +36,7 @@ export async function getTranslationPaths({ cwd, glob }: { cwd: string; glob: st const entryFullPath = resolve(cwd, entry); const pluginBasePath = dirname(entryFullPath); try { - const content = await readFileAsync(entryFullPath, 'utf8'); + const content = await readFile(entryFullPath, 'utf8'); const { translations } = JSON.parse(content) as I18NRCFileStructure; if (translations && translations.length) { translations.forEach((translation) => { diff --git a/src/core/server/i18n/i18n_config.ts b/src/core/server/i18n/i18n_config.ts new file mode 100644 index 0000000000000..f181c52dc4b06 --- /dev/null +++ b/src/core/server/i18n/i18n_config.ts @@ -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 { schema, TypeOf } from '@kbn/config-schema'; + +export const config = { + path: 'i18n', + schema: schema.object({ + locale: schema.string({ defaultValue: 'en' }), + }), +}; + +export type I18nConfigType = TypeOf; diff --git a/src/core/server/i18n/i18n_service.mock.ts b/src/core/server/i18n/i18n_service.mock.ts new file mode 100644 index 0000000000000..679751aefbf27 --- /dev/null +++ b/src/core/server/i18n/i18n_service.mock.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 { PublicMethodsOf } from '@kbn/utility-types'; +import type { I18nServiceSetup, I18nService } from './i18n_service'; + +const createSetupContractMock = () => { + const mock: jest.Mocked = { + getLocale: jest.fn(), + getTranslationFiles: jest.fn(), + }; + + mock.getLocale.mockReturnValue('en'); + mock.getTranslationFiles.mockReturnValue([]); + + return mock; +}; + +type I18nServiceContract = PublicMethodsOf; + +const createMock = () => { + const mock: jest.Mocked = { + setup: jest.fn(), + }; + + mock.setup.mockResolvedValue(createSetupContractMock()); + + return mock; +}; + +export const i18nServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/i18n/i18n_service.test.mocks.ts b/src/core/server/i18n/i18n_service.test.mocks.ts new file mode 100644 index 0000000000000..23f97a1404fff --- /dev/null +++ b/src/core/server/i18n/i18n_service.test.mocks.ts @@ -0,0 +1,28 @@ +/* + * 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 getKibanaTranslationFilesMock = jest.fn(); +jest.doMock('./get_kibana_translation_files', () => ({ + getKibanaTranslationFiles: getKibanaTranslationFilesMock, +})); + +export const initTranslationsMock = jest.fn(); +jest.doMock('./init_translations', () => ({ + initTranslations: initTranslationsMock, +})); diff --git a/src/core/server/i18n/i18n_service.test.ts b/src/core/server/i18n/i18n_service.test.ts new file mode 100644 index 0000000000000..87de39a92ab26 --- /dev/null +++ b/src/core/server/i18n/i18n_service.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { getKibanaTranslationFilesMock, initTranslationsMock } from './i18n_service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import { I18nService } from './i18n_service'; + +import { configServiceMock } from '../config/mocks'; +import { mockCoreContext } from '../core_context.mock'; + +const getConfigService = (locale = 'en') => { + const configService = configServiceMock.create(); + configService.atPath.mockImplementation((path) => { + if (path === 'i18n') { + return new BehaviorSubject({ + locale, + }); + } + return new BehaviorSubject({}); + }); + return configService; +}; + +describe('I18nService', () => { + let service: I18nService; + let configService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + configService = getConfigService(); + + const coreContext = mockCoreContext.create({ configService }); + service = new I18nService(coreContext); + }); + + describe('#setup', () => { + it('calls `getKibanaTranslationFiles` with the correct parameters', async () => { + getKibanaTranslationFilesMock.mockResolvedValue([]); + + const pluginPaths = ['/pathA', '/pathB']; + await service.setup({ pluginPaths }); + + expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1); + expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths); + }); + + it('calls `initTranslations` with the correct parameters', async () => { + const translationFiles = ['/path/to/file', 'path/to/another/file']; + getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); + + await service.setup({ pluginPaths: [] }); + + expect(initTranslationsMock).toHaveBeenCalledTimes(1); + expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles); + }); + + it('returns accessors for locale and translation files', async () => { + const translationFiles = ['/path/to/file', 'path/to/another/file']; + getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); + + const { getLocale, getTranslationFiles } = await service.setup({ pluginPaths: [] }); + + expect(getLocale()).toEqual('en'); + expect(getTranslationFiles()).toEqual(translationFiles); + }); + }); +}); diff --git a/src/core/server/i18n/i18n_service.ts b/src/core/server/i18n/i18n_service.ts new file mode 100644 index 0000000000000..fd32dd7fdd6ef --- /dev/null +++ b/src/core/server/i18n/i18n_service.ts @@ -0,0 +1,75 @@ +/* + * 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 { take } from 'rxjs/operators'; +import { Logger } from '../logging'; +import { IConfigService } from '../config'; +import { CoreContext } from '../core_context'; +import { config as i18nConfigDef, I18nConfigType } from './i18n_config'; +import { getKibanaTranslationFiles } from './get_kibana_translation_files'; +import { initTranslations } from './init_translations'; + +interface SetupDeps { + pluginPaths: string[]; +} + +/** + * @public + */ +export interface I18nServiceSetup { + /** + * Return the locale currently in use. + */ + getLocale(): string; + + /** + * Return the absolute paths to translation files currently in use. + */ + getTranslationFiles(): string[]; +} + +export class I18nService { + private readonly log: Logger; + private readonly configService: IConfigService; + + constructor(coreContext: CoreContext) { + this.log = coreContext.logger.get('i18n'); + this.configService = coreContext.configService; + } + + public async setup({ pluginPaths }: SetupDeps): Promise { + const i18nConfig = await this.configService + .atPath(i18nConfigDef.path) + .pipe(take(1)) + .toPromise(); + + const locale = i18nConfig.locale; + this.log.debug(`Using locale: ${locale}`); + + const translationFiles = await getKibanaTranslationFiles(locale, pluginPaths); + + this.log.debug(`Using translation files: [${translationFiles.join(', ')}]`); + await initTranslations(locale, translationFiles); + + return { + getLocale: () => locale, + getTranslationFiles: () => translationFiles, + }; + } +} diff --git a/src/legacy/server/i18n/index.ts b/src/core/server/i18n/index.ts similarity index 86% rename from src/legacy/server/i18n/index.ts rename to src/core/server/i18n/index.ts index a7ef49f44532c..2db3fdeb405d9 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/core/server/i18n/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { i18nMixin } from './i18n_mixin'; +export { config, I18nConfigType } from './i18n_config'; +export { I18nService, I18nServiceSetup } from './i18n_service'; diff --git a/src/core/server/i18n/init_translations.ts b/src/core/server/i18n/init_translations.ts new file mode 100644 index 0000000000000..94e6d41019ad2 --- /dev/null +++ b/src/core/server/i18n/init_translations.ts @@ -0,0 +1,31 @@ +/* + * 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 { i18n, i18nLoader } from '@kbn/i18n'; + +export const initTranslations = async (locale: string, translationFiles: string[]) => { + i18nLoader.registerTranslationFiles(translationFiles); + const translations = await i18nLoader.getTranslationsByLocale(locale); + i18n.init( + Object.freeze({ + locale, + ...translations, + }) + ); +}; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0adda4770639d..7b19c3a686757 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -64,6 +64,7 @@ import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; +import { I18nServiceSetup } from './i18n'; // Because of #79265 we need to explicity import, then export these types for // scripts/telemetry_check.js to work as expected @@ -336,6 +337,8 @@ export { MetricsServiceStart, } from './metrics'; +export { I18nServiceSetup } from './i18n'; + export { AppCategory } from '../types'; export { DEFAULT_APP_CATEGORIES } from '../utils'; @@ -421,6 +424,8 @@ export interface CoreSetup = KbnServer as any; @@ -75,6 +76,7 @@ beforeEach(() => { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), elasticsearch: { legacy: {} } as any, + i18n: i18nServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), http: { ...httpServiceMock.createInternalSetupContract(), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index c42771179aba2..3111c8daf7981 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -251,6 +251,7 @@ export class LegacyService implements CoreService { csp: setupDeps.core.http.csp, getServerInfo: setupDeps.core.http.getServerInfo, }, + i18n: setupDeps.core.i18n, logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index 0d9e9af39317c..6faccc9c9da56 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -19,7 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { MetricsService } from './metrics_service'; +import type { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 8ca0c82219ed4..03a0ae2d6443a 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -38,6 +38,7 @@ import { metricsServiceMock } from './metrics/metrics_service.mock'; import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; +import { i18nServiceMock } from './i18n/i18n_service.mock'; export { configServiceMock } from './config/mocks'; export { httpServerMock } from './http/http_server.mocks'; @@ -57,6 +58,7 @@ export { statusServiceMock } from './status/status_service.mock'; export { contextServiceMock } from './context/context_service.mock'; export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; +export { i18nServiceMock } from './i18n/i18n_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -136,6 +138,7 @@ function createCoreSetupMock({ context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetup(), http: httpMock, + i18n: i18nServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), uiSettings: uiSettingsMock, @@ -172,6 +175,7 @@ function createInternalCoreSetupMock() { savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), environment: environmentServiceMock.createSetupContract(), + i18n: i18nServiceMock.createSetupContract(), httpResources: httpResourcesMock.createSetupContract(), rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 22e79741e854c..3b2634ddbe315 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -176,6 +176,7 @@ export function createPluginSetupContext( csp: deps.http.csp, getServerInfo: deps.http.getServerInfo, }, + i18n: deps.i18n, logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 14d6de889dd42..3ea8badb0f450 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PluginsService, PluginsServiceSetup } from './plugins_service'; +import type { PluginsService, PluginsServiceSetup } from './plugins_service'; type PluginsServiceMock = jest.Mocked>; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 64a382e539fb0..02b82c17ed4fc 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -127,6 +127,7 @@ async function testSetup(options: { isDevClusterMaster?: boolean } = {}) { [mockPluginSystem] = MockPluginsSystem.mock.instances as any; mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + mockPluginSystem.getPlugins.mockReturnValue([]); environmentSetup = environmentServiceMock.createSetupContract(); } @@ -469,6 +470,22 @@ describe('PluginsService', () => { deprecationProvider ); }); + + it('returns the paths of the plugins', async () => { + const pluginA = createPlugin('A', { path: '/plugin-A-path', configPath: 'pathA' }); + const pluginB = createPlugin('B', { path: '/plugin-B-path', configPath: 'pathB' }); + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([]), + }); + + mockPluginSystem.getPlugins.mockReturnValue([pluginA, pluginB]); + + const { pluginPaths } = await pluginsService.discover({ environment: environmentSetup }); + + expect(pluginPaths).toEqual(['/plugin-A-path', '/plugin-B-path']); + }); }); describe('#generateUiPluginsConfigs()', () => { @@ -633,6 +650,7 @@ describe('PluginService when isDevClusterMaster is true', () => { await expect(pluginsService.discover({ environment: environmentSetup })).resolves .toMatchInlineSnapshot(` Object { + "pluginPaths": Array [], "pluginTree": undefined, "uiPlugins": Object { "browserConfigs": Map {}, diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index a1062bde7765f..5967e6d5358de 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -118,6 +118,7 @@ export class PluginsService implements CoreService plugin.path), uiPlugins: { internal: this.uiPluginInternalInfo, public: uiPlugins, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index ae9267ca5cf60..89a3697ebe9cd 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -92,6 +92,15 @@ test('can be setup even without plugins', async () => { expect(pluginsSetup.size).toBe(0); }); +test('getPlugins returns the list of plugins', () => { + const pluginA = createPlugin('plugin-a'); + const pluginB = createPlugin('plugin-b'); + pluginsSystem.addPlugin(pluginA); + pluginsSystem.addPlugin(pluginB); + + expect(pluginsSystem.getPlugins()).toEqual([pluginA, pluginB]); +}); + test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] })); pluginsSystem.addPlugin( diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 72d2cfe158b37..23dc77b7bf673 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -42,6 +42,10 @@ export class PluginsSystem { this.plugins.set(plugin.name, plugin); } + public getPlugins() { + return [...this.plugins.values()]; + } + /** * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index 01d084f9ae53c..00e617de82542 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -17,8 +17,8 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { RenderingService as Service } from '../rendering_service'; -import { InternalRenderingServiceSetup } from '../types'; +import type { RenderingService as Service } from '../rendering_service'; +import type { InternalRenderingServiceSetup } from '../types'; import { mockRenderingServiceParams } from './params'; type IRenderingService = PublicMethodsOf; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index c56cdabf6e4cd..85dbf4b5e8c6a 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -19,8 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; - -import { +import type { SavedObjectsService, InternalSavedObjectsServiceSetup, InternalSavedObjectsServiceStart, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 52500673f7f31..88d7fecbcf502 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -480,6 +480,8 @@ export interface CoreSetup HttpServerInfo; } +// @public (undocumented) +export interface I18nServiceSetup { + getLocale(): string; + getTranslationFiles(): string[]; +} + // @public export type IBasePath = Pick; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index fe299c6d11675..d2d6990dc5451 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -100,3 +100,9 @@ export const mockLoggingService = loggingServiceMock.create(); jest.doMock('./logging/logging_service', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); + +import { i18nServiceMock } from './i18n/i18n_service.mock'; +export const mockI18nService = i18nServiceMock.create(); +jest.doMock('./i18n/i18n_service', () => ({ + I18nService: jest.fn(() => mockI18nService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 78703ceeec7ae..0c7ebbcb527ec 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -31,6 +31,7 @@ import { mockMetricsService, mockStatusService, mockLoggingService, + mockI18nService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -49,6 +50,7 @@ beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, + pluginPaths: [], uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -70,6 +72,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -83,6 +86,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); expect(mockStatusService.setup).toHaveBeenCalledTimes(1); expect(mockLoggingService.setup).toHaveBeenCalledTimes(1); + expect(mockI18nService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -96,6 +100,7 @@ test('injects legacy dependency to context#setup()', async () => { ]); mockPluginsService.discover.mockResolvedValue({ pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, + pluginPaths: [], uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); @@ -185,6 +190,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -207,6 +213,7 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); }); test(`doesn't validate config if env.isDevClusterMaster is true`, async () => { diff --git a/src/core/server/server.ts b/src/core/server/server.ts index eaa03d11cab98..55ed88e55a9f5 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ + import apm from 'elastic-apm-node'; import { config as pathConfig } from '@kbn/utils'; import { mapToObject } from '@kbn/std'; import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; import { CoreApp } from './core_app'; +import { I18nService } from './i18n'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; @@ -29,10 +31,11 @@ import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; -import { SavedObjectsService } from '../server/saved_objects'; +import { SavedObjectsService } from './saved_objects'; import { MetricsService, opsConfig } from './metrics'; import { CapabilitiesService } from './capabilities'; import { EnvironmentService, config as pidConfig } from './environment'; +// do not try to shorten the import to `./status`, it will break server test mocking import { StatusService } from './status/status_service'; import { config as cspConfig } from './csp'; @@ -44,6 +47,7 @@ import { config as kibanaConfig } from './kibana_config'; import { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects'; import { config as uiSettingsConfig } from './ui_settings'; import { config as statusConfig } from './status'; +import { config as i18nConfig } from './i18n'; import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; @@ -72,6 +76,7 @@ export class Server { private readonly logging: LoggingService; private readonly coreApp: CoreApp; private readonly coreUsageData: CoreUsageDataService; + private readonly i18n: I18nService; #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; @@ -103,6 +108,7 @@ export class Server { this.httpResources = new HttpResourcesService(core); this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); + this.i18n = new I18nService(core); } public async setup() { @@ -112,7 +118,7 @@ export class Server { const environmentSetup = await this.environment.setup(); // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. - const { pluginTree, uiPlugins } = await this.plugins.discover({ + const { pluginTree, pluginPaths, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); const legacyConfigSetup = await this.legacy.setupLegacyConfig(); @@ -125,6 +131,9 @@ export class Server { await ensureValidConfiguration(this.configService, legacyConfigSetup); } + // setup i18n prior to any other service, to have translations ready + const i18nServiceSetup = await this.i18n.setup({ pluginPaths }); + const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: // 1) Can access context from any KP plugin @@ -190,6 +199,7 @@ export class Server { elasticsearch: elasticsearchServiceSetup, environment: environmentSetup, http: httpSetup, + i18n: i18nServiceSetup, savedObjects: savedObjectsSetup, status: statusSetup, uiSettings: uiSettingsSetup, @@ -302,6 +312,7 @@ export class Server { opsConfig, statusConfig, pidConfig, + i18nConfig, ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 0ee2d03229a78..42892ddbb490c 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { StatusService } from './status_service'; +import type { StatusService } from './status_service'; import { InternalStatusServiceSetup, StatusServiceSetup, diff --git a/src/core/server/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts index b1ed0dd188cde..818e9b43889e3 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -22,7 +22,7 @@ import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart, } from './types'; -import { UiSettingsService } from './ui_settings_service'; +import type { UiSettingsService } from './ui_settings_service'; const createClientMock = () => { const mocked: jest.Mocked = { diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index e1f03b8a08847..39df3990ff2ff 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -128,21 +128,12 @@ export default () => cGroupOverrides: HANDLED_IN_NEW_PLATFORM, }).default(), - // still used by the legacy i18n mixin - plugins: Joi.object({ - paths: Joi.array().items(Joi.string()).default([]), - scanDirs: Joi.array().items(Joi.string()).default([]), - initialize: Joi.boolean().default(true), - }).default(), - + plugins: HANDLED_IN_NEW_PLATFORM, path: HANDLED_IN_NEW_PLATFORM, stats: HANDLED_IN_NEW_PLATFORM, status: HANDLED_IN_NEW_PLATFORM, map: HANDLED_IN_NEW_PLATFORM, - - i18n: Joi.object({ - locale: Joi.string().default('en'), - }).default(), + i18n: HANDLED_IN_NEW_PLATFORM, // temporarily moved here from the (now deleted) kibana legacy plugin kibana: Joi.object({ diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.test.ts b/src/legacy/server/i18n/get_kibana_translation_paths.test.ts deleted file mode 100644 index 0f202c4d433c0..0000000000000 --- a/src/legacy/server/i18n/get_kibana_translation_paths.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 { I18N_RC } from './constants'; -import { fromRoot } from '../../../core/server/utils'; - -jest.mock('./get_translation_paths', () => ({ getTranslationPaths: jest.fn() })); -import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; -import { getTranslationPaths as mockGetTranslationPaths } from './get_translation_paths'; - -describe('getKibanaTranslationPaths', () => { - const mockConfig = { get: jest.fn() }; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('calls getTranslationPaths against kibana root and kibana-extra', async () => { - mockConfig.get.mockReturnValue([]); - await getKibanaTranslationPaths(mockConfig); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(1, { - cwd: fromRoot('.'), - glob: `*/${I18N_RC}`, - }); - - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { - cwd: fromRoot('../kibana-extra'), - glob: `*/${I18N_RC}`, - }); - }); - - it('calls getTranslationPaths for each config returned in plugin.paths and plugins.scanDirs', async () => { - mockConfig.get.mockReturnValueOnce(['a', 'b']).mockReturnValueOnce(['c']); - await getKibanaTranslationPaths(mockConfig); - expect(mockConfig.get).toHaveBeenNthCalledWith(1, 'plugins.paths'); - expect(mockConfig.get).toHaveBeenNthCalledWith(2, 'plugins.scanDirs'); - - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { cwd: 'a', glob: I18N_RC }); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(3, { cwd: 'b', glob: I18N_RC }); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(4, { cwd: 'c', glob: `*/${I18N_RC}` }); - }); -}); diff --git a/src/legacy/server/i18n/i18n_mixin.ts b/src/legacy/server/i18n/i18n_mixin.ts deleted file mode 100644 index 0b3879073c164..0000000000000 --- a/src/legacy/server/i18n/i18n_mixin.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { i18n, i18nLoader } from '@kbn/i18n'; -import { basename } from 'path'; -import { Server } from '@hapi/hapi'; -import type { UsageCollectionSetup } from '../../../plugins/usage_collection/server'; -import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; -import KbnServer, { KibanaConfig } from '../kbn_server'; -import { registerLocalizationUsageCollector } from './localization'; - -export async function i18nMixin( - kbnServer: KbnServer, - server: Server, - config: Pick -) { - const locale = config.get('i18n.locale') as string; - - const translationPaths = await getKibanaTranslationPaths(config); - - const currentTranslationPaths = ([] as string[]) - .concat(...translationPaths) - .filter((translationPath) => basename(translationPath, '.json') === locale); - i18nLoader.registerTranslationFiles(currentTranslationPaths); - - const translations = await i18nLoader.getTranslationsByLocale(locale); - i18n.init( - Object.freeze({ - locale, - ...translations, - }) - ); - - const getTranslationsFilePaths = () => currentTranslationPaths; - - server.decorate('server', 'getTranslationsFilePaths', getTranslationsFilePaths); - - if (kbnServer.newPlatform.setup.plugins.usageCollection) { - const { usageCollection } = kbnServer.newPlatform.setup.plugins as { - usageCollection: UsageCollectionSetup; - }; - registerLocalizationUsageCollector(usageCollection, { - getLocale: () => config.get('i18n.locale') as string, - getTranslationsFilePaths, - }); - } -} diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index e29563a7c6266..013da35d2acb7 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -31,7 +31,6 @@ import warningsMixin from './warnings'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import { uiMixin } from '../ui'; -import { i18nMixin } from './i18n'; /** * @typedef {import('./kbn_server').KibanaConfig} KibanaConfig @@ -82,9 +81,6 @@ export default class KbnServer { loggingMixin, warningsMixin, - // scan translations dirs, register locale files and initialize i18n engine. - i18nMixin, - // tell the config we are done loading plugins configCompleteMixin, diff --git a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap index c479562795512..c782ce9c8cc84 100644 --- a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap +++ b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap @@ -13,3 +13,5 @@ exports[`kibana_usage_collection Runs the setup method without issues 5`] = `fal exports[`kibana_usage_collection Runs the setup method without issues 6`] = `true`; exports[`kibana_usage_collection Runs the setup method without issues 7`] = `false`; + +exports[`kibana_usage_collection Runs the setup method without issues 8`] = `true`; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 2408dc84c2e56..f3b7d8ca5eea0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -24,3 +24,4 @@ export { registerKibanaUsageCollector } from './kibana'; export { registerOpsStatsCollector } from './ops_stats'; export { registerCspCollector } from './csp'; export { registerCoreUsageCollector } from './core'; +export { registerLocalizationUsageCollector } from './localization'; diff --git a/src/legacy/server/i18n/localization/file_integrity.test.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.mocks.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.test.mocks.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.mocks.ts diff --git a/src/legacy/server/i18n/localization/file_integrity.test.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.ts diff --git a/src/legacy/server/i18n/localization/file_integrity.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.ts diff --git a/src/legacy/server/i18n/localization/index.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/index.ts similarity index 100% rename from src/legacy/server/i18n/localization/index.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/index.ts diff --git a/src/legacy/server/i18n/localization/telemetry_localization_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.test.ts similarity index 100% rename from src/legacy/server/i18n/localization/telemetry_localization_collector.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.test.ts diff --git a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts similarity index 82% rename from src/legacy/server/i18n/localization/telemetry_localization_collector.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts index fb837f5ae28df..f2b00bd629a07 100644 --- a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts @@ -20,6 +20,7 @@ import { i18nLoader } from '@kbn/i18n'; import { size } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { I18nServiceSetup } from '../../../../../core/server'; import { getIntegrityHashes, Integrities } from './file_integrity'; export interface UsageStats { @@ -28,11 +29,6 @@ export interface UsageStats { labelsCount?: number; } -export interface LocalizationUsageCollectorHelpers { - getLocale: () => string; - getTranslationsFilePaths: () => string[]; -} - export async function getTranslationCount( loader: typeof i18nLoader, locale: string @@ -41,13 +37,10 @@ export async function getTranslationCount( return size(translations.messages); } -export function createCollectorFetch({ - getLocale, - getTranslationsFilePaths, -}: LocalizationUsageCollectorHelpers) { +export function createCollectorFetch({ getLocale, getTranslationFiles }: I18nServiceSetup) { return async function fetchUsageStats(): Promise { const locale = getLocale(); - const translationFilePaths: string[] = getTranslationsFilePaths(); + const translationFilePaths: string[] = getTranslationFiles(); const [labelsCount, integrities] = await Promise.all([ getTranslationCount(i18nLoader, locale), @@ -62,15 +55,14 @@ export function createCollectorFetch({ }; } -// TODO: Migrate out of the Legacy dir export function registerLocalizationUsageCollector( usageCollection: UsageCollectionSetup, - helpers: LocalizationUsageCollectorHelpers + i18n: I18nServiceSetup ) { const collector = usageCollection.makeUsageCollector({ type: 'localization', isReady: () => true, - fetch: createCollectorFetch(helpers), + fetch: createCollectorFetch(i18n), schema: { locale: { type: 'keyword' }, integrities: { DYNAMIC_KEY: { type: 'text' } }, diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 198fdbb7a8703..16cb620351aaa 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -41,6 +41,7 @@ import { registerUiMetricUsageCollector, registerCspCollector, registerCoreUsageCollector, + registerLocalizationUsageCollector, } from './collectors'; interface KibanaUsageCollectionPluginsDepsSetup { @@ -104,5 +105,6 @@ export class KibanaUsageCollectionPlugin implements Plugin { ); registerCspCollector(usageCollection, coreSetup.http); registerCoreUsageCollector(usageCollection, getCoreUsageDataService); + registerLocalizationUsageCollector(usageCollection, coreSetup.i18n); } } diff --git a/src/plugins/telemetry/schema/legacy_plugins.json b/src/plugins/telemetry/schema/legacy_plugins.json index 1a7c0ccb15082..d5b0514b64918 100644 --- a/src/plugins/telemetry/schema/legacy_plugins.json +++ b/src/plugins/telemetry/schema/legacy_plugins.json @@ -1,21 +1,3 @@ { - "properties": { - "localization": { - "properties": { - "locale": { - "type": "keyword" - }, - "integrities": { - "properties": { - "DYNAMIC_KEY": { - "type": "text" - } - } - }, - "labelsCount": { - "type": "long" - } - } - } - } + "properties": {} } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c840cbe8fc94d..a1eae69ffaed0 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1581,6 +1581,23 @@ } } }, + "localization": { + "properties": { + "locale": { + "type": "keyword" + }, + "integrities": { + "properties": { + "DYNAMIC_KEY": { + "type": "text" + } + } + }, + "labelsCount": { + "type": "long" + } + } + }, "stack_management": { "properties": { "visualize:enableLabs": { From e909cee7f1a9206dbd3822fe92b36890b29dec6c Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 10 Nov 2020 09:49:20 -0500 Subject: [PATCH 12/19] [Maps] add on-prem EMS config (#82525) Adds.a new `map.emsUrl` setting. User can configure this to the location of a local on-prem installation of EMS. For this setting to take effect, the cluster must be configured with an enterprise license. --- .../maps_legacy/common/ems_defaults.ts | 25 ++ src/plugins/maps_legacy/common/index.ts | 20 ++ src/plugins/maps_legacy/config.ts | 19 +- src/plugins/maps_legacy/kibana.json | 1 + src/plugins/maps_legacy/public/index.ts | 1 + .../public/map/service_settings.js | 2 +- src/plugins/maps_legacy/server/index.ts | 1 + .../plugins/maps/common/ems_settings.test.ts | 221 ++++++++++++++++++ x-pack/plugins/maps/common/ems_settings.ts | 91 ++++++++ .../ems_boundaries_layer_wizard.tsx | 5 +- .../ems_base_map_layer_wizard.tsx | 5 +- .../components/ems_unavailable_message.tsx | 7 +- x-pack/plugins/maps/public/kibana_services.ts | 17 +- .../plugins/maps/public/licensed_features.ts | 8 + x-pack/plugins/maps/public/meta.test.js | 42 +++- x-pack/plugins/maps/public/meta.ts | 31 ++- x-pack/plugins/maps/public/plugin.ts | 11 +- .../bootstrap/get_initial_layers.test.js | 18 +- .../routing/bootstrap/get_initial_layers.ts | 4 +- x-pack/plugins/maps/server/plugin.ts | 20 +- x-pack/plugins/maps/server/routes.js | 80 ++++--- 21 files changed, 532 insertions(+), 97 deletions(-) create mode 100644 src/plugins/maps_legacy/common/ems_defaults.ts create mode 100644 src/plugins/maps_legacy/common/index.ts create mode 100644 x-pack/plugins/maps/common/ems_settings.test.ts create mode 100644 x-pack/plugins/maps/common/ems_settings.ts diff --git a/src/plugins/maps_legacy/common/ems_defaults.ts b/src/plugins/maps_legacy/common/ems_defaults.ts new file mode 100644 index 0000000000000..583dca1dbf036 --- /dev/null +++ b/src/plugins/maps_legacy/common/ems_defaults.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// Default config for the elastic hosted EMS endpoints +export const DEFAULT_EMS_FILE_API_URL = 'https://vector.maps.elastic.co'; +export const DEFAULT_EMS_TILE_API_URL = 'https://tiles.maps.elastic.co'; +export const DEFAULT_EMS_LANDING_PAGE_URL = 'https://maps.elastic.co/v7.10'; +export const DEFAULT_EMS_FONT_LIBRARY_URL = + 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf'; diff --git a/src/plugins/maps_legacy/common/index.ts b/src/plugins/maps_legacy/common/index.ts new file mode 100644 index 0000000000000..12148ec1ec6b8 --- /dev/null +++ b/src/plugins/maps_legacy/common/index.ts @@ -0,0 +1,20 @@ +/* + * 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 * from './ems_defaults'; diff --git a/src/plugins/maps_legacy/config.ts b/src/plugins/maps_legacy/config.ts index f49d56dedd45f..68595944e68b3 100644 --- a/src/plugins/maps_legacy/config.ts +++ b/src/plugins/maps_legacy/config.ts @@ -21,18 +21,29 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { configSchema as tilemapSchema } from '../tile_map/config'; import { configSchema as regionmapSchema } from '../region_map/config'; +import { + DEFAULT_EMS_FONT_LIBRARY_URL, + DEFAULT_EMS_LANDING_PAGE_URL, + DEFAULT_EMS_TILE_API_URL, + DEFAULT_EMS_FILE_API_URL, +} from './common/ems_defaults'; + export const configSchema = schema.object({ includeElasticMapsService: schema.boolean({ defaultValue: true }), proxyElasticMapsServiceInMaps: schema.boolean({ defaultValue: false }), tilemap: tilemapSchema, regionmap: regionmapSchema, manifestServiceUrl: schema.string({ defaultValue: '' }), - emsFileApiUrl: schema.string({ defaultValue: 'https://vector.maps.elastic.co' }), - emsTileApiUrl: schema.string({ defaultValue: 'https://tiles.maps.elastic.co' }), - emsLandingPageUrl: schema.string({ defaultValue: 'https://maps.elastic.co/v7.10' }), + + emsUrl: schema.string({ defaultValue: '' }), + + emsFileApiUrl: schema.string({ defaultValue: DEFAULT_EMS_FILE_API_URL }), + emsTileApiUrl: schema.string({ defaultValue: DEFAULT_EMS_TILE_API_URL }), + emsLandingPageUrl: schema.string({ defaultValue: DEFAULT_EMS_LANDING_PAGE_URL }), emsFontLibraryUrl: schema.string({ - defaultValue: 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf', + defaultValue: DEFAULT_EMS_FONT_LIBRARY_URL, }), + emsTileLayerId: schema.object({ bright: schema.string({ defaultValue: 'road_map' }), desaturated: schema.string({ defaultValue: 'road_map_desaturated' }), diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index d9bf33e661368..1499b3de446b5 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -5,5 +5,6 @@ "configPath": ["map"], "ui": true, "server": true, + "extraPublicDirs": ["common"], "requiredBundles": ["kibanaReact", "charts"] } diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index fe5338b890ec8..2654ded907cce 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -59,6 +59,7 @@ export { mapTooltipProvider, }; +export * from '../common'; export * from './common/types'; export { ORIGIN } from './common/constants/origin'; diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 833304378402a..7a00456b89a92 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -128,7 +128,7 @@ export class ServiceSettings { allServices.push(tmsService); } - if (this._mapConfig.includeElasticMapsService) { + if (this._mapConfig.includeElasticMapsService && !this._mapConfig.emsUrl) { const servicesFromManifest = await this._emsClient.getTMSServices(); const strippedServiceFromManifest = await Promise.all( servicesFromManifest diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 665b3b8986ef0..ba37df60f9357 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -30,6 +30,7 @@ export const config: PluginConfigDescriptor = { tilemap: true, regionmap: true, manifestServiceUrl: true, + emsUrl: true, emsFileApiUrl: true, emsTileApiUrl: true, emsLandingPageUrl: true, diff --git a/x-pack/plugins/maps/common/ems_settings.test.ts b/x-pack/plugins/maps/common/ems_settings.test.ts new file mode 100644 index 0000000000000..69ae7069129cb --- /dev/null +++ b/x-pack/plugins/maps/common/ems_settings.test.ts @@ -0,0 +1,221 @@ +/* + * 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 { EMSSettings, IEMSConfig } from './ems_settings'; +import { + DEFAULT_EMS_FILE_API_URL, + DEFAULT_EMS_FONT_LIBRARY_URL, + DEFAULT_EMS_LANDING_PAGE_URL, + DEFAULT_EMS_TILE_API_URL, +} from '../../../../src/plugins/maps_legacy/common'; + +const IS_ENTERPRISE_PLUS = () => true; + +describe('EMSSettings', () => { + const mockConfig: IEMSConfig = { + includeElasticMapsService: true, + proxyElasticMapsServiceInMaps: false, + emsUrl: '', + emsFileApiUrl: DEFAULT_EMS_FILE_API_URL, + emsTileApiUrl: DEFAULT_EMS_TILE_API_URL, + emsLandingPageUrl: DEFAULT_EMS_LANDING_PAGE_URL, + emsFontLibraryUrl: DEFAULT_EMS_FONT_LIBRARY_URL, + isEMSEnabled: true, + }; + + describe('isEMSEnabled/isOnPrem', () => { + test('should validate defaults', () => { + const emsSettings = new EMSSettings(mockConfig, IS_ENTERPRISE_PLUS); + expect(emsSettings.isEMSEnabled()).toBe(true); + expect(emsSettings.isOnPrem()).toBe(false); + }); + + test('should validate if on-prem is turned on', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.isEMSEnabled()).toBe(true); + expect(emsSettings.isOnPrem()).toBe(true); + }); + + test('should not validate if ems turned off', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + includeElasticMapsService: false, + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.isEMSEnabled()).toBe(false); + expect(emsSettings.isOnPrem()).toBe(false); + }); + + test('should work if ems is turned off, but on-prem is turned on', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + includeElasticMapsService: false, + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.isEMSEnabled()).toBe(true); + expect(emsSettings.isOnPrem()).toBe(true); + }); + + describe('when license is turned off', () => { + test('should not be enabled', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + }, + }, + () => false + ); + expect(emsSettings.isEMSEnabled()).toBe(false); + expect(emsSettings.isOnPrem()).toBe(true); + }); + }); + }); + + describe('emsUrl setting', () => { + describe('when emsUrl is not set', () => { + test('should respect defaults', () => { + const emsSettings = new EMSSettings(mockConfig, IS_ENTERPRISE_PLUS); + expect(emsSettings.getEMSFileApiUrl()).toBe(DEFAULT_EMS_FILE_API_URL); + expect(emsSettings.getEMSTileApiUrl()).toBe(DEFAULT_EMS_TILE_API_URL); + expect(emsSettings.getEMSFontLibraryUrl()).toBe(DEFAULT_EMS_FONT_LIBRARY_URL); + expect(emsSettings.getEMSLandingPageUrl()).toBe(DEFAULT_EMS_LANDING_PAGE_URL); + }); + test('should apply overrides', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsFileApiUrl: 'https://file.foobar', + emsTileApiUrl: 'https://tile.foobar', + emsFontLibraryUrl: 'https://tile.foobar/font', + emsLandingPageUrl: 'https://maps.foobar/v7.666', + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.getEMSFileApiUrl()).toBe('https://file.foobar'); + expect(emsSettings.getEMSTileApiUrl()).toBe('https://tile.foobar'); + expect(emsSettings.getEMSFontLibraryUrl()).toBe('https://tile.foobar/font'); + expect(emsSettings.getEMSLandingPageUrl()).toBe('https://maps.foobar/v7.666'); + }); + }); + + describe('when emsUrl is set', () => { + test('should override defaults', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.getEMSFileApiUrl()).toBe('https://localhost:8080/file'); + expect(emsSettings.getEMSTileApiUrl()).toBe('https://localhost:8080/tile'); + expect(emsSettings.getEMSFontLibraryUrl()).toBe( + 'https://localhost:8080/tile/fonts/{fontstack}/{range}.pbf' + ); + expect(emsSettings.getEMSLandingPageUrl()).toBe('https://localhost:8080/maps'); + }); + + describe('internal settings overrides (the below behavior is not publically supported, but aids internal debugging use-cases)', () => { + test(`should override internal emsFileApiUrl`, () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + emsFileApiUrl: 'https://file.foobar', + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.getEMSFileApiUrl()).toBe('https://file.foobar'); + expect(emsSettings.getEMSTileApiUrl()).toBe('https://localhost:8080/tile'); + expect(emsSettings.getEMSFontLibraryUrl()).toBe( + 'https://localhost:8080/tile/fonts/{fontstack}/{range}.pbf' + ); + expect(emsSettings.getEMSLandingPageUrl()).toBe('https://localhost:8080/maps'); + }); + + test(`should override internal emsTileApiUrl`, () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + emsTileApiUrl: 'https://tile.foobar', + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.getEMSFileApiUrl()).toBe('https://localhost:8080/file'); + expect(emsSettings.getEMSTileApiUrl()).toBe('https://tile.foobar'); + expect(emsSettings.getEMSFontLibraryUrl()).toBe( + 'https://localhost:8080/tile/fonts/{fontstack}/{range}.pbf' + ); + expect(emsSettings.getEMSLandingPageUrl()).toBe('https://localhost:8080/maps'); + }); + + test('should override internal emsFontLibraryUrl', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + emsFontLibraryUrl: 'https://maps.foobar/fonts', + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.getEMSFileApiUrl()).toBe('https://localhost:8080/file'); + expect(emsSettings.getEMSTileApiUrl()).toBe('https://localhost:8080/tile'); + expect(emsSettings.getEMSFontLibraryUrl()).toBe('https://maps.foobar/fonts'); + expect(emsSettings.getEMSLandingPageUrl()).toBe('https://localhost:8080/maps'); + }); + + test('should override internal emsLandingPageUrl', () => { + const emsSettings = new EMSSettings( + { + ...mockConfig, + ...{ + emsUrl: 'https://localhost:8080', + emsLandingPageUrl: 'https://maps.foobar', + }, + }, + IS_ENTERPRISE_PLUS + ); + expect(emsSettings.getEMSFileApiUrl()).toBe('https://localhost:8080/file'); + expect(emsSettings.getEMSTileApiUrl()).toBe('https://localhost:8080/tile'); + expect(emsSettings.getEMSFontLibraryUrl()).toBe( + 'https://localhost:8080/tile/fonts/{fontstack}/{range}.pbf' + ); + expect(emsSettings.getEMSLandingPageUrl()).toBe('https://maps.foobar'); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/maps/common/ems_settings.ts b/x-pack/plugins/maps/common/ems_settings.ts new file mode 100644 index 0000000000000..0f4d211f0e963 --- /dev/null +++ b/x-pack/plugins/maps/common/ems_settings.ts @@ -0,0 +1,91 @@ +/* + * 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 { + DEFAULT_EMS_FILE_API_URL, + DEFAULT_EMS_FONT_LIBRARY_URL, + DEFAULT_EMS_LANDING_PAGE_URL, + DEFAULT_EMS_TILE_API_URL, +} from '../../../../src/plugins/maps_legacy/common'; + +export interface IEMSConfig { + emsUrl?: string; + includeElasticMapsService?: boolean; + proxyElasticMapsServiceInMaps?: boolean; + emsFileApiUrl?: string; + emsTileApiUrl?: string; + emsLandingPageUrl?: string; + emsFontLibraryUrl?: string; + isEMSEnabled?: boolean; +} + +export class EMSSettings { + private readonly _config: IEMSConfig; + private readonly _getIsEnterprisePlus: () => boolean; + + constructor(config: IEMSConfig, getIsEnterPrisePlus: () => boolean) { + this._config = config; + this._getIsEnterprisePlus = getIsEnterPrisePlus; + } + + _isEMSUrlSet() { + return !!this._config.emsUrl; + } + + _getEMSRoot() { + return this._config.emsUrl!.replace(/\/$/, ''); + } + + isOnPrem(): boolean { + return this._isEMSUrlSet(); + } + + isIncludeElasticMapsService() { + return !!this._config.includeElasticMapsService; + } + + isEMSEnabled(): boolean { + if (this._isEMSUrlSet()) { + return this._getIsEnterprisePlus(); + } + return this.isIncludeElasticMapsService(); + } + + getEMSFileApiUrl(): string { + if (this._config.emsFileApiUrl !== DEFAULT_EMS_FILE_API_URL || !this._isEMSUrlSet()) { + return this._config.emsFileApiUrl!; + } else { + return `${this._getEMSRoot()}/file`; + } + } + + isProxyElasticMapsServiceInMaps(): boolean { + return !!this._config.proxyElasticMapsServiceInMaps; + } + + getEMSTileApiUrl(): string { + if (this._config.emsTileApiUrl !== DEFAULT_EMS_TILE_API_URL || !this._isEMSUrlSet()) { + return this._config.emsTileApiUrl!; + } else { + return `${this._getEMSRoot()}/tile`; + } + } + getEMSLandingPageUrl(): string { + if (this._config.emsLandingPageUrl !== DEFAULT_EMS_LANDING_PAGE_URL || !this._isEMSUrlSet()) { + return this._config.emsLandingPageUrl!; + } else { + return `${this._getEMSRoot()}/maps`; + } + } + + getEMSFontLibraryUrl(): string { + if (this._config.emsFontLibraryUrl !== DEFAULT_EMS_FONT_LIBRARY_URL || !this._isEMSUrlSet()) { + return this._config.emsFontLibraryUrl!; + } else { + return `${this._getEMSRoot()}/tile/fonts/{fontstack}/{range}.pbf`; + } + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index 8d4d57e524276..768bbd1d94700 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -11,14 +11,15 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re import { EMSFileCreateSourceEditor } from './create_source_editor'; import { EMSFileSource, sourceTitle } from './ems_file_source'; // @ts-ignore -import { getIsEmsEnabled } from '../../../kibana_services'; +import { getEMSSettings } from '../../../kibana_services'; import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const emsBoundariesLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { - return getIsEmsEnabled(); + const emsSettings = getEMSSettings(); + return emsSettings!.isEMSEnabled(); }, description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 315759a2eba29..bfa46574f007a 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -13,13 +13,14 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source'; import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_layer'; // @ts-ignore import { TileServiceSelect } from './tile_service_select'; -import { getIsEmsEnabled } from '../../../kibana_services'; +import { getEMSSettings } from '../../../kibana_services'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const emsBaseMapLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { - return getIsEmsEnabled(); + const emsSettings = getEMSSettings(); + return emsSettings!.isEMSEnabled(); }, description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', diff --git a/x-pack/plugins/maps/public/components/ems_unavailable_message.tsx b/x-pack/plugins/maps/public/components/ems_unavailable_message.tsx index dea161fafd609..ba897b7d9da0c 100644 --- a/x-pack/plugins/maps/public/components/ems_unavailable_message.tsx +++ b/x-pack/plugins/maps/public/components/ems_unavailable_message.tsx @@ -5,15 +5,14 @@ */ import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { getIsEmsEnabled } from '../kibana_services'; +import { getEMSSettings } from '../kibana_services'; export function getEmsUnavailableMessage(): string { - const isEmsEnabled = getIsEmsEnabled(); + const isEmsEnabled = getEMSSettings().isEMSEnabled(); if (isEmsEnabled) { return i18n.translate('xpack.maps.source.ems.noAccessDescription', { defaultMessage: - 'Kibana is unable to access Elastic Maps Service. Contact your system administrator', + 'Kibana is unable to access Elastic Maps Service. Contact your system administrator.', }); } diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 08ee4b6628dd1..782c37a72d99b 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -9,6 +9,7 @@ import { CoreStart } from 'kibana/public'; import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { MapsConfigType } from '../config'; import { MapsPluginStartDependencies } from './plugin'; +import { EMSSettings } from '../common/ems_settings'; let kibanaVersion: string; export const setKibanaVersion = (version: string) => (kibanaVersion = version); @@ -62,14 +63,16 @@ let kibanaCommonConfig: MapsLegacyConfig; export const setKibanaCommonConfig = (config: MapsLegacyConfig) => (kibanaCommonConfig = config); export const getKibanaCommonConfig = () => kibanaCommonConfig; -export const getIsEmsEnabled = () => getKibanaCommonConfig().includeElasticMapsService; -export const getEmsFontLibraryUrl = () => getKibanaCommonConfig().emsFontLibraryUrl; +let emsSettings: EMSSettings; +export const setEMSSettings = (value: EMSSettings) => { + emsSettings = value; +}; +export const getEMSSettings = () => { + return emsSettings; +}; + export const getEmsTileLayerId = () => getKibanaCommonConfig().emsTileLayerId; -export const getEmsFileApiUrl = () => getKibanaCommonConfig().emsFileApiUrl; -export const getEmsTileApiUrl = () => getKibanaCommonConfig().emsTileApiUrl; -export const getEmsLandingPageUrl = () => getKibanaCommonConfig().emsLandingPageUrl; -export const getProxyElasticMapsServiceInMaps = () => - getKibanaCommonConfig().proxyElasticMapsServiceInMaps; + export const getRegionmapLayers = () => _.get(getKibanaCommonConfig(), 'regionmap.layers', []); export const getTilemap = () => _.get(getKibanaCommonConfig(), 'tilemap', []); diff --git a/x-pack/plugins/maps/public/licensed_features.ts b/x-pack/plugins/maps/public/licensed_features.ts index 67fa526da0cbd..13809f2b26a8c 100644 --- a/x-pack/plugins/maps/public/licensed_features.ts +++ b/x-pack/plugins/maps/public/licensed_features.ts @@ -27,9 +27,13 @@ export const LICENCED_FEATURES_DETAILS: Record licenseId; export const getIsGoldPlus = () => isGoldPlus; +export const getIsEnterprisePlus = () => isEnterprisePlus; + export function registerLicensedFeatures(licensingPlugin: LicensingPluginSetup) { for (const licensedFeature of Object.values(LICENSED_FEATURES)) { licensingPlugin.featureUsage.register( @@ -45,6 +49,10 @@ export function setLicensingPluginStart(licensingPlugin: LicensingPluginStart) { licensingPluginStart.license$.subscribe((license: ILicense) => { const gold = license.check(APP_ID, 'gold'); isGoldPlus = gold.state === 'valid'; + + const enterprise = license.check(APP_ID, 'enterprise'); + isEnterprisePlus = enterprise.state === 'valid'; + licenseId = license.uid; }); } diff --git a/x-pack/plugins/maps/public/meta.test.js b/x-pack/plugins/maps/public/meta.test.js index c414c8a2d400e..d4f9885830b54 100644 --- a/x-pack/plugins/maps/public/meta.test.js +++ b/x-pack/plugins/maps/public/meta.test.js @@ -9,14 +9,22 @@ import { getEMSClient, getGlyphUrl } from './meta'; jest.mock('@elastic/ems-client'); +const EMS_FONTS_URL_MOCK = 'ems/fonts'; +const MOCK_EMS_SETTINGS = { + isEMSEnabled: () => true, + getEMSFileApiUrl: () => 'https://file-api', + getEMSTileApiUrl: () => 'https://tile-api', + getEMSLandingPageUrl: () => 'http://test.com', + getEMSFontLibraryUrl: () => EMS_FONTS_URL_MOCK, + isProxyElasticMapsServiceInMaps: () => false, +}; + describe('default use without proxy', () => { beforeEach(() => { - require('./kibana_services').getProxyElasticMapsServiceInMaps = () => false; - require('./kibana_services').getIsEmsEnabled = () => true; require('./kibana_services').getEmsTileLayerId = () => '123'; - require('./kibana_services').getEmsFileApiUrl = () => 'https://file-api'; - require('./kibana_services').getEmsTileApiUrl = () => 'https://tile-api'; - require('./kibana_services').getEmsLandingPageUrl = () => 'http://test.com'; + require('./kibana_services').getEMSSettings = () => { + return MOCK_EMS_SETTINGS; + }; require('./licensed_features').getLicenseId = () => { return 'foobarlicenseid'; }; @@ -32,10 +40,7 @@ describe('default use without proxy', () => { describe('getGlyphUrl', () => { describe('EMS enabled', () => { - const EMS_FONTS_URL_MOCK = 'ems/fonts'; beforeAll(() => { - require('./kibana_services').getIsEmsEnabled = () => true; - require('./kibana_services').getEmsFontLibraryUrl = () => EMS_FONTS_URL_MOCK; require('./kibana_services').getHttp = () => ({ basePath: { prepend: (url) => url, // No need to actually prepend a dev basepath for test @@ -45,7 +50,12 @@ describe('getGlyphUrl', () => { describe('EMS proxy enabled', () => { beforeAll(() => { - require('./kibana_services').getProxyElasticMapsServiceInMaps = () => true; + require('./kibana_services').getEMSSettings = () => { + return { + ...MOCK_EMS_SETTINGS, + isProxyElasticMapsServiceInMaps: () => true, + }; + }; }); test('should return proxied EMS fonts URL', async () => { @@ -55,7 +65,12 @@ describe('getGlyphUrl', () => { describe('EMS proxy disabled', () => { beforeAll(() => { - require('./kibana_services').getProxyElasticMapsServiceInMaps = () => false; + require('./kibana_services').getEMSSettings = () => { + return { + ...MOCK_EMS_SETTINGS, + isProxyElasticMapsServiceInMaps: () => false, + }; + }; }); test('should return EMS fonts URL', async () => { @@ -72,7 +87,12 @@ describe('getGlyphUrl', () => { }, }; require('./kibana_services').getHttp = () => mockHttp; - require('./kibana_services').getIsEmsEnabled = () => false; + require('./kibana_services').getEMSSettings = () => { + return { + ...MOCK_EMS_SETTINGS, + isEMSEnabled: () => false, + }; + }; }); test('should return kibana fonts URL', async () => { diff --git a/x-pack/plugins/maps/public/meta.ts b/x-pack/plugins/maps/public/meta.ts index 929050338de72..5632e226478a7 100644 --- a/x-pack/plugins/maps/public/meta.ts +++ b/x-pack/plugins/maps/public/meta.ts @@ -18,15 +18,10 @@ import { } from '../common/constants'; import { getHttp, - getIsEmsEnabled, getRegionmapLayers, getTilemap, - getEmsFileApiUrl, - getEmsTileApiUrl, - getEmsLandingPageUrl, - getEmsFontLibraryUrl, - getProxyElasticMapsServiceInMaps, getKibanaVersion, + getEMSSettings, } from './kibana_services'; import { getLicenseId } from './licensed_features'; import { LayerConfig } from '../../../../src/plugins/region_map/config'; @@ -40,7 +35,7 @@ export function getKibanaTileMap(): unknown { } export async function getEmsFileLayers(): Promise { - if (!getIsEmsEnabled()) { + if (!getEMSSettings().isEMSEnabled()) { return []; } @@ -48,7 +43,7 @@ export async function getEmsFileLayers(): Promise { } export async function getEmsTmsServices(): Promise { - if (!getIsEmsEnabled()) { + if (!getEMSSettings().isEMSEnabled()) { return []; } @@ -65,18 +60,18 @@ let emsClient: EMSClient | null = null; let latestLicenseId: string | undefined; export function getEMSClient(): EMSClient { if (!emsClient) { - const proxyElasticMapsServiceInMaps = getProxyElasticMapsServiceInMaps(); + const emsSettings = getEMSSettings(); const proxyPath = ''; - const tileApiUrl = proxyElasticMapsServiceInMaps + const tileApiUrl = emsSettings!.isProxyElasticMapsServiceInMaps() ? relativeToAbsolute( getHttp().basePath.prepend(`/${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}`) ) - : getEmsTileApiUrl(); - const fileApiUrl = proxyElasticMapsServiceInMaps + : emsSettings!.getEMSTileApiUrl(); + const fileApiUrl = emsSettings!.isProxyElasticMapsServiceInMaps() ? relativeToAbsolute( getHttp().basePath.prepend(`/${GIS_API_PATH}/${EMS_FILES_CATALOGUE_PATH}`) ) - : getEmsFileApiUrl(); + : emsSettings!.getEMSFileApiUrl(); emsClient = new EMSClient({ language: i18n.getLocale(), @@ -84,7 +79,7 @@ export function getEMSClient(): EMSClient { appName: EMS_APP_NAME, tileApiUrl, fileApiUrl, - landingPageUrl: getEmsLandingPageUrl(), + landingPageUrl: emsSettings!.getEMSLandingPageUrl(), fetchFunction(url: string) { return fetch(url); }, @@ -100,16 +95,18 @@ export function getEMSClient(): EMSClient { } export function getGlyphUrl(): string { - if (!getIsEmsEnabled()) { + const emsSettings = getEMSSettings(); + if (!emsSettings!.isEMSEnabled()) { return getHttp().basePath.prepend(`/${FONTS_API_PATH}/{fontstack}/{range}`); } - return getProxyElasticMapsServiceInMaps() + + return emsSettings!.isProxyElasticMapsServiceInMaps() ? relativeToAbsolute( getHttp().basePath.prepend( `/${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}/${EMS_GLYPHS_PATH}` ) ) + `/{fontstack}/{range}` - : getEmsFontLibraryUrl(); + : emsSettings!.getEMSFontLibraryUrl(); } export function isRetina(): boolean { diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 75a3f8ef5ede8..b79a2b06b9b37 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -19,6 +19,7 @@ import { // @ts-ignore import { MapView } from './inspector/views/map_view'; import { + setEMSSettings, setKibanaCommonConfig, setKibanaVersion, setMapAppConfig, @@ -55,7 +56,12 @@ import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { StartContract as FileUploadStartContract } from '../../file_upload/public'; import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; -import { registerLicensedFeatures, setLicensingPluginStart } from './licensed_features'; +import { + getIsEnterprisePlus, + registerLicensedFeatures, + setLicensingPluginStart, +} from './licensed_features'; +import { EMSSettings } from '../common/ems_settings'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -111,6 +117,9 @@ export class MapsPlugin setMapAppConfig(config); setKibanaVersion(this._initializerContext.env.packageInfo.version); + const emsSettings = new EMSSettings(plugins.mapsLegacy.config, getIsEnterprisePlus); + setEMSSettings(emsSettings); + // register url generators const getStartServices = async () => { const [coreStart] = await core.getStartServices(); diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.test.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.test.js index 4de29e6f028e1..66adb1da6900e 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.test.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.test.js @@ -15,7 +15,11 @@ const layerListNotProvided = undefined; describe('Saved object has layer list', () => { beforeEach(() => { - require('../../kibana_services').getIsEmsEnabled = () => true; + require('../../kibana_services').getEMSSettings = () => { + return { + isEMSEnabled: () => true, + }; + }; }); it('Should get initial layers from saved object', () => { @@ -65,7 +69,11 @@ describe('EMS is enabled', () => { require('../../meta').getKibanaTileMap = () => { return null; }; - require('../../kibana_services').getIsEmsEnabled = () => true; + require('../../kibana_services').getEMSSettings = () => { + return { + isEMSEnabled: () => true, + }; + }; require('../../kibana_services').getEmsTileLayerId = () => ({ bright: 'road_map', desaturated: 'road_map_desaturated', @@ -101,7 +109,11 @@ describe('EMS is not enabled', () => { require('../../meta').getKibanaTileMap = () => { return null; }; - require('../../kibana_services').getIsEmsEnabled = () => false; + require('../../kibana_services').getEMSSettings = () => { + return { + isEMSEnabled: () => false, + }; + }; }); it('Should return empty layer list since there are no configured tile layers', () => { diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts index e828dc88409cb..c887320873995 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts @@ -23,7 +23,7 @@ import { TileLayer } from '../../classes/layers/tile_layer/tile_layer'; import { EMSTMSSource } from '../../classes/sources/ems_tms_source'; // @ts-expect-error import { VectorTileLayer } from '../../classes/layers/vector_tile_layer/vector_tile_layer'; -import { getIsEmsEnabled, getToasts } from '../../kibana_services'; +import { getEMSSettings, getToasts } from '../../kibana_services'; import { INITIAL_LAYERS_KEY } from '../../../common/constants'; import { getKibanaTileMap } from '../../meta'; @@ -39,7 +39,7 @@ export function getInitialLayers(layerListJSON?: string, initialLayers: LayerDes return [layerDescriptor, ...initialLayers]; } - const isEmsEnabled = getIsEmsEnabled(); + const isEmsEnabled = getEMSSettings()!.isEMSEnabled(); if (isEmsEnabled) { const layerDescriptor = VectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor({ isAutoSelect: true }), diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 00950e96047a0..65d79272494f0 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -28,7 +28,7 @@ import { ILicense } from '../../licensing/common/types'; import { LicensingPluginSetup } from '../../licensing/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { MapsLegacyPluginSetup } from '../../../../src/plugins/maps_legacy/server'; -import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; +import { EMSSettings } from '../common/ems_settings'; interface SetupDeps { features: FeaturesPluginSetupContract; @@ -52,7 +52,7 @@ export class MapsPlugin implements Plugin { _initHomeData( home: HomeServerPluginSetup, prependBasePath: (path: string) => string, - mapsLegacyConfig: MapsLegacyConfig + emsSettings: EMSSettings ) { const sampleDataLinkLabel = i18n.translate('xpack.maps.sampleDataLinkLabel', { defaultMessage: 'Map', @@ -125,7 +125,7 @@ export class MapsPlugin implements Plugin { home.tutorials.registerTutorial( emsBoundariesSpecProvider({ prependBasePath, - emsLandingPageUrl: mapsLegacyConfig.emsLandingPageUrl, + emsLandingPageUrl: emsSettings.getEMSLandingPageUrl(), }) ); } @@ -148,21 +148,27 @@ export class MapsPlugin implements Plugin { } let routesInitialized = false; + let isEnterprisePlus = false; + const emsSettings = new EMSSettings(mapsLegacyConfig, () => isEnterprisePlus); licensing.license$.subscribe((license: ILicense) => { - const { state } = license.check('maps', 'basic'); - if (state === 'valid' && !routesInitialized) { + const basic = license.check(APP_ID, 'basic'); + + const enterprise = license.check(APP_ID, 'enterprise'); + isEnterprisePlus = enterprise.state === 'valid'; + + if (basic.state === 'valid' && !routesInitialized) { routesInitialized = true; initRoutes( core.http.createRouter(), license.uid, - mapsLegacyConfig, + emsSettings, this.kibanaVersion, this._logger ); } }); - this._initHomeData(home, core.http.basePath.prepend, mapsLegacyConfig); + this._initHomeData(home, core.http.basePath.prepend, emsSettings); features.registerKibanaFeature({ id: APP_ID, diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 5feacfee4d4d2..49d646f9a4e6d 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -33,38 +33,45 @@ import fs from 'fs'; import path from 'path'; import { initMVTRoutes } from './mvt/mvt_routes'; -export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { +const EMPTY_EMS_CLIENT = { + async getFileLayers() { + return []; + }, + async getTMSServices() { + return []; + }, + async getMainManifest() { + return null; + }, + async getDefaultFileManifest() { + return null; + }, + async getDefaultTMSManifest() { + return null; + }, + addQueryParams() {}, +}; + +export function initRoutes(router, licenseUid, emsSettings, kbnVersion, logger) { let emsClient; - if (mapConfig.includeElasticMapsService) { + + if (emsSettings.isIncludeElasticMapsService()) { emsClient = new EMSClient({ language: i18n.getLocale(), appVersion: kbnVersion, appName: EMS_APP_NAME, - fileApiUrl: mapConfig.emsFileApiUrl, - tileApiUrl: mapConfig.emsTileApiUrl, - landingPageUrl: mapConfig.emsLandingPageUrl, + fileApiUrl: emsSettings.getEMSFileApiUrl(), + tileApiUrl: emsSettings.getEMSTileApiUrl(), + landingPageUrl: emsSettings.getEMSLandingPageUrl(), fetchFunction: fetch, }); emsClient.addQueryParams({ license: licenseUid }); } else { - emsClient = { - async getFileLayers() { - return []; - }, - async getTMSServices() { - return []; - }, - async getMainManifest() { - return null; - }, - async getDefaultFileManifest() { - return null; - }, - async getDefaultTMSManifest() { - return null; - }, - addQueryParams() {}, - }; + emsClient = EMPTY_EMS_CLIENT; + } + + function getEMSClient() { + return emsSettings.isEMSEnabled() ? emsClient : EMPTY_EMS_CLIENT; } router.get( @@ -90,7 +97,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return null; } - const fileLayers = await emsClient.getFileLayers(); + const fileLayers = await getEMSClient().getFileLayers(); const layer = fileLayers.find((layer) => layer.getId() === request.query.id); if (!layer) { return null; @@ -127,7 +134,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return null; } - const tmsServices = await emsClient.getTMSServices(); + const tmsServices = await getEMSClient().getTMSServices(); const tmsService = tmsServices.find((layer) => layer.getId() === request.query.id); if (!tmsService) { return null; @@ -153,7 +160,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const main = await emsClient.getMainManifest(); + const main = await getEMSClient().getMainManifest(); const proxiedManifest = { services: [], }; @@ -189,8 +196,8 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const file = await emsClient.getDefaultFileManifest(); //need raw manifest - const fileLayers = await emsClient.getFileLayers(); + const file = await getEMSClient().getDefaultFileManifest(); //need raw manifest + const fileLayers = await getEMSClient().getFileLayers(); const layers = file.layers.map((layerJson) => { const newLayerJson = { ...layerJson }; @@ -231,7 +238,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const tilesManifest = await emsClient.getDefaultTMSManifest(); + const tilesManifest = await getEMSClient().getDefaultTMSManifest(); const newServices = tilesManifest.services.map((service) => { const newService = { ...service, @@ -284,7 +291,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return null; } - const tmsServices = await emsClient.getTMSServices(); + const tmsServices = await getEMSClient().getTMSServices(); const tmsService = tmsServices.find((layer) => layer.getId() === request.query.id); if (!tmsService) { return null; @@ -319,7 +326,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const tmsServices = await emsClient.getTMSServices(); + const tmsServices = await getEMSClient().getTMSServices(); const tmsService = tmsServices.find((layer) => layer.getId() === request.query.id); if (!tmsService) { return null; @@ -368,7 +375,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const tmsServices = await emsClient.getTMSServices(); + const tmsServices = await getEMSClient().getTMSServices(); const tmsService = tmsServices.find((layer) => layer.getId() === request.query.id); if (!tmsService) { return null; @@ -409,7 +416,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return response.badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const tmsServices = await emsClient.getTMSServices(); + const tmsServices = await getEMSClient().getTMSServices(); const tmsService = tmsServices.find((layer) => layer.getId() === request.query.id); if (!tmsService) { return null; @@ -439,7 +446,8 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { if (!checkEMSProxyEnabled()) { return response.badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const url = mapConfig.emsFontLibraryUrl + const url = emsSettings + .getEMSFontLibraryUrl() .replace('{fontstack}', request.params.fontstack) .replace('{range}', request.params.range); @@ -469,7 +477,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return response.badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const tmsServices = await emsClient.getTMSServices(); + const tmsServices = await getEMSClient().getTMSServices(); const tmsService = tmsServices.find((layer) => layer.getId() === request.params.id); if (!tmsService) { return null; @@ -573,7 +581,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { ); function checkEMSProxyEnabled() { - const proxyEMSInMaps = mapConfig.proxyElasticMapsServiceInMaps; + const proxyEMSInMaps = emsSettings.isProxyElasticMapsServiceInMaps(); if (!proxyEMSInMaps) { logger.warn( `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off` From 1b86d79319efa8c5b153a90099d20babde208b92 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 10 Nov 2020 09:11:21 -0600 Subject: [PATCH 13/19] [DOCS] Consolidates drilldown pages (#82081) * [DOCS] Consolidated drilldowns * Review comments pt 1 * Update docs/user/dashboard/drilldowns.asciidoc Co-authored-by: Anton Dosov * Fixes supported drilldowns link * Update src/core/public/doc_links/doc_links_service.ts Co-authored-by: Anton Dosov * Fixes rogue disable section and fixes intro formatting * Fixes URL drilldown link Co-authored-by: Anton Dosov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../dashboard/dashboard-drilldown.asciidoc | 96 ------- docs/user/dashboard/drilldowns.asciidoc | 269 ++++++++++++++++-- docs/user/dashboard/url-drilldown.asciidoc | 115 +------- .../public/doc_links/doc_links_service.ts | 6 +- 4 files changed, 249 insertions(+), 237 deletions(-) delete mode 100644 docs/user/dashboard/dashboard-drilldown.asciidoc diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc deleted file mode 100644 index bdff7355d7467..0000000000000 --- a/docs/user/dashboard/dashboard-drilldown.asciidoc +++ /dev/null @@ -1,96 +0,0 @@ -[[dashboard-drilldown]] -=== Dashboard drilldown - -The dashboard drilldown allows you to navigate from one dashboard to another dashboard. -For example, you might have a dashboard that shows the overall status of multiple data centers. -You can create a drilldown that navigates from this dashboard to a dashboard -that shows a single data center or server. - -This example shows a dashboard panel that contains a pie chart with a configured dashboard drilldown: - -[role="screenshot"] -image::images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] - -[float] -[[dashboard-drilldown-supported-panels]] -==== Supported panels - -The following panels support dashboard drilldowns: - -* Lens -* Area -* Data table -* Heat map -* Horizontal bar -* Line -* Maps -* Pie -* TSVB -* Tag cloud -* Timelion -* Vega -* Vertical bar - -[float] -[[drilldowns-example]] -==== Try it: Create a dashboard drilldown - -Create the *Host Overview* drilldown shown above. - -*Set up the dashboards* - -. Add the sample web logs data set. - -. Create a new dashboard, called `Host Overview`, and include these visualizations -from the sample data set: -+ -[%hardbreaks] -*[Logs] Heatmap* -*[Logs] Visitors by OS* -*[Logs] Host, Visits, and Bytes Table* -*[Logs] Total Requests and Bytes* -+ -TIP: If you don’t see data for a panel, try changing the time range. - -. Open the *[Logs] Web traffic* dashboard. - -. Set a search and filter. -+ -[%hardbreaks] -Search: `extension.keyword: ("gz" or "css" or "deb")` -Filter: `geo.src: CN` - - -*Create the drilldown* - - -. In the dashboard menu bar, click *Edit*. - -. In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. - -. Pick *Go to dashboard* action. - -. Give the drilldown a name. - -. Select *Host Overview* as the destination dashboard. - -. Keep both filters enabled so that the drilldown carries over the global filters and date range. -+ -Your input should look similar to this: -+ -[role="screenshot"] -image::images/drilldown_create.png[Create drilldown with entries for drilldown name and destination] - -. Click *Create drilldown.* - -. Save the dashboard. -+ -If you don’t save the drilldown, and then navigate away, the drilldown is lost. - -. In *[Logs] Visitors by OS*, click the `win 8` slice of the pie, and then select the name of your drilldown. -+ -[role="screenshot"] -image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard] -+ -You are navigated to your destination dashboard. Verify that the search query, filters, -and time range are carried over. diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index e3d0e16630c5c..ca788020d9286 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,52 +1,259 @@ [role="xpack"] [[drilldowns]] -== Use drilldowns for dashboard actions +== Create custom dashboard actions -Drilldowns, also known as custom actions, allow you to configure a -workflow for analyzing and troubleshooting your data. -For example, using a drilldown, you can navigate from one dashboard to another, -taking the current time range, filters, and other parameters with you, -so the context remains the same. You can continue your analysis from a new perspective. +Custom dashboard actions, also known as drilldowns, allow you to create +workflows for analyzing and troubleshooting your data. Drilldowns apply only to the panel that you created the drilldown from, and are not shared across all of the panels. Each panel can have multiple drilldowns. -[role="screenshot"] -image::images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] +Third-party developers can create drilldowns. To learn how to code drilldowns, refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin]. + +[float] +[[supported-drilldowns]] +=== Supported drilldowns + +{kib} supports two types of drilldowns. + +[NOTE] +============================================== +Some drilldowns are paid subscription features, while others are free. +For a comparison of the Elastic subscription levels, +refer https://www.elastic.co/subscriptions[the subscription page]. +============================================== + +[float] +[[dashboard-drilldown]] +==== Dashboard drilldowns + +Dashboard drilldowns enable you to open a dashboard from another dashboard, +taking the time range, filters, and other parameters with you, +so the context remains the same. Dashboard drilldowns help you to continue your analysis from a new perspective. + +For example, if you have a dashboard that shows the overall status of multiple data center, +you can create a drilldown that navigates from the overall status dashboard to a dashboard +that shows a single data center or server. + +[float] +[[url-drilldown]] +==== URL drilldowns + +beta[] URL drilldowns enable you to navigate from a dashboard to internal or external URLs. +Destination URLs can be dynamic, depending on the dashboard context or user interaction with a panel. +For example, if you have a dashboard that shows data from a Github repository, you can create a URL drilldown +that opens Github from the dashboard. + +Some panels support multiple interactions, also known as triggers. +The <> you use to create a <> depends on the trigger you choose. URL drilldowns support these types of triggers: + +* *Single click* — A single data point in the visualization. + +* *Range selection* — A range of values in a visualization. + +For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. + +To disable URL drilldowns on your {kib} instance, disable the plugin: -Drilldowns are specific to the dashboard panel for which you create them—they are not shared across panels. A panel can have multiple drilldowns. +["source","yml"] +----------- +url_drilldown.enabled: false +----------- [float] -[[actions]] -=== Drilldown actions +[[dashboard-drilldown-supported-panels]] +=== Supported panels -Drilldowns are user-configurable {kib} actions that are stored with the dashboard metadata. -Kibana provides the following types of actions: +The following panels support dashboard and URL drilldowns. -[cols="2"] +[options="header"] |=== -a| <> +| Panel | Dashboard drilldown | URL drilldown -| Navigate to a dashboard. +| Lens +^| X +^| X -a| <> +| Area +^| X +^| X -| Navigate to external or internal URL. +| Controls +^| +^| + +| Data Table +^| X +^| X + +| Gauge +^| +^| + +| Goal +^| +^| + +| Heat map +^| X +^| X + +| Horizontal Bar +^| X +^| X + +| Line +^| X +^| X + +| Maps +^| X +^| + +| Markdown +^| +^| + +| Metric +^| +^| + +| Pie +^| X +^| X + +| TSVB +^| X +^| + +| Tag Cloud +^| X +^| X + +| Timelion +^| X +^| + +| Vega +^| X +^| + +| Vertical Bar +^| X +^| X |=== -[NOTE] -============================================== -Some action types are paid commercial features, while others are free. -For a comparison of the Elastic subscription levels, -see https://www.elastic.co/subscriptions[the subscription page]. -============================================== +[float] +[[drilldowns-example]] +=== Try it: Create a dashboard drilldown + +To create dashboard drilldowns, you create or locate the dashboards you want to connect, then configure the drilldown that allows you to easily open one dashboard from the other dashboard. + +image:images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] [float] -[[code-drilldowns]] -=== Code drilldowns -Third-party developers can create drilldowns. -Refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin] -to learn how to code drilldowns. +==== Create the dashboard -include::dashboard-drilldown.asciidoc[] -include::url-drilldown.asciidoc[] +. Add the *Sample web logs* data. + +. Create a new dashboard, then add the following panels: + +* *[Logs] Heatmap* +* *[Logs] Host, Visits, and Bytes Table* +* *[Logs] Total Requests and Bytes* +* *[Logs] Visitors by OS* ++ +If you don’t see data for a panel, try changing the <>. + +. Save the dashboard. In the *Title* field, enter `Host Overview`. + +. Open the *[Logs] Web traffic* dashboard. + +. Set a search and filter. ++ +[%hardbreaks] +Search: `extension.keyword: ("gz" or "css" or "deb")` +Filter: `geo.src: CN` + +[float] +==== Create the drilldown + +. In the toolbar, click *Edit*. + +. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*. + +. Give the drilldown a name, then select *Go to dashboard*. + +. From the *Choose a destination dashboard* dropdown, select *Host Overview*. + +. To carry over the filter, query, and date range, make sure that *Use filters and query from origin dashboard* and *Use date range from origin dashboard* are selected. ++ +[role="screenshot"] +image::images/drilldown_create.png[Create drilldown with entries for drilldown name and destination] + +. Click *Create drilldown*. ++ +The drilldown is stored as dashboard metadata. + +. Save the dashboard. ++ +If you fail to save the dashboard, the drilldown is lost when you navigate away from the dashboard. + +. In the *[Logs] Visitors by OS* panel, click *win 8*, then select the drilldown. ++ +[role="screenshot"] +image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard] +. On the *Host Overview* dashboard, verify that the search query, filters, +and date range are carried over. + +[float] +[[create-a-url-drilldown]] +=== Try it: Create a URL drilldown + +beta[] To create URL drilldowns, you add <> to a URL template, which configures the bahavior of the drilldown. + +image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github] + +. Add the *Sample web logs* data. + +. Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but works for demonstration purposes. + +. In the toolbar, click *Edit*. + +. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*. + +.. In the *Name* field, enter `Show on Github`. + +.. Select *Go to URL*. + +.. Enter the URL template: ++ +[source, bash] +---- +https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} +---- ++ +The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice. In *URL preview*, `{{event.value}}` is substituted with a <> value. ++ +[role="screenshot"] +image:images/url_drilldown_url_template.png[URL template input] + +.. Click *Create drilldown*. ++ +The drilldown is stored as dashboard metadata. + +. Save the dashboard. ++ +If you fail to save the dashboard, the drilldown is lost when you navigate away from the dashboard. + +. On the *[Logs] Visitors by OS* panel, click any chart slice, then select *Show on Github*. ++ +[role="screenshot"] +image:images/url_drilldown_popup.png[URL drilldown popup] + +. On the page that lists the issues in the {kib} repository, verify the slice value appears in Github. ++ +[role="screenshot"] +image:images/url_drilldown_github.png[Github] + +include::url-drilldown.asciidoc[] diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 212e29898bd40..872d83bfd9009 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -1,96 +1,9 @@ -[[url-drilldown]] -=== URL drilldown +[[url_templating]] +=== URL templating beta[] -The URL drilldown allows you to navigate from a dashboard to an internal or external URL. -The destination URL can be dynamic, depending on the dashboard context or user’s interaction with a visualization. - -For example, you might have a dashboard that shows data from a Github repository. -You can create a drilldown that navigates from this dashboard to Github. - -[role="screenshot"] -image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github] - -NOTE: URL drilldown is available with the https://www.elastic.co/subscriptions[Gold subscription] and higher. - -[float] -[[url-drilldown-supported-panels]] -==== Supported panels - -The following panels support URL drilldowns: - -* Lens -* Area -* Data table -* Heat map -* Horizontal bar -* Line -* Pie -* Tag cloud -* Vertical bar - -[float] -[[try-it]] -==== Try it: Create a URL drilldown - -This example shows how to create the "Show on Github" drilldown shown above. - -. Add the sample web logs data set. -. Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but it should work for demonstration purposes. -. In the dashboard menu bar, click *Edit*. -. In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. -. Give the drilldown a name: *Show on Github*. -. Select a drilldown action: *Go to URL*. -+ -[role="screenshot"] -image:images/url_drilldown_pick_an_action.png[Action picker] -. Enter a URL template: -+ -[source, bash] ----- -https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} ----- -+ -This example URL navigates to {kib} issues on Github. `{{event.value}}` will be substituted with a value associated with a clicked pie slice. In _preview_ `{{event.value}}` is substituted with a <> value. -[role="screenshot"] -image:images/url_drilldown_url_template.png[URL template input] -. Click *Create drilldown*. -. Save the dashboard. -+ -If you don’t save the drilldown, and then navigate away, the drilldown is lost. - -. In *[Logs] Visitors by OS*, click any slice of the pie, and then select the drilldown *Show on Github*. -+ -[role="screenshot"] -image:images/url_drilldown_popup.png[URL drilldown popup] -+ -You are navigated to the issue list in the {kib} repository. Verify that value from a pie slice you’ve clicked on is carried over to Github. -+ -[role="screenshot"] -image:images/url_drilldown_github.png[Github] - -[float] -[[trigger-picker]] -==== Picking a trigger for a URL drilldown - -Some panels support multiple user interactions (called triggers) for which you can configure a URL drilldown. The list of supported variables in the URL template depends on the trigger you selected. -In the preceding example, you configured a URL drilldown on a pie chart. The only trigger that pie chart supports is clicking on a pie slice, so you didn’t have to pick a trigger. - -However, the sample *[Logs] Unique Visitors vs. Average Bytes* chart supports both clicking on a data point and selecting a range. When you create a URL drilldown for this chart, you have the following choices: - -[role="screenshot"] -image:images/url_drilldown_trigger_picker.png[Trigger picker: Single click and Range selection] - -Variables in the URL template differ per trigger. -For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. -You can create multiple URL drilldowns per panel and attach them to different triggers. - -[float] -[[templating]] -==== URL templating language - -The URL template input uses Handlebars — a simple templating language. Handlebars templates look like regular text with embedded Handlebars expressions. +The URL template input uses https://handlebarsjs.com/guide/expressions.html#expressions[Handlebars] — a simple templating language. Handlebars templates look like regular text with embedded Handlebars expressions. [source, bash] ---- @@ -99,14 +12,13 @@ https://github.com/elastic/kibana/issues?q={{event.value}} A Handlebars expression is a `{{`, some contents, followed by a `}}`. When the drilldown is executed, these expressions are replaced by values from the dashboard and interaction context. -Refer to Handlebars https://ela.st/handlebars-docs#expressions[documentation] to learn about advanced use cases. - [[helpers]] -In addition to https://ela.st/handlebars-helpers[built-in] Handlebars helpers, you can use the following custom helpers: +In addition to https://handlebarsjs.com/guide/builtin-helpers.html[built-in] Handlebars helpers, you can use custom helpers. +Refer to Handlebars https://ela.st/handlebars-docs#expressions[documentation] to learn about advanced use cases. |=== -|Helper |Use case +|Custom helper |Use case |json a|Serialize variables in JSON format. @@ -133,7 +45,7 @@ a|Format dates. Supports relative dates expressions (for example, "now-15d"). R Example: -`{{ date event.from “YYYY MM DD”}}` + +`{{date event.from “YYYY MM DD”}}` + `{{date “now-15”}}` |formatNumber @@ -240,7 +152,7 @@ For example, `{{context.panel.filters}}` are previewed with the current filters *Event* variables are extracted during drilldown execution from a user interaction with a panel (for example, from a pie slice that the user clicked on). Because there is no user interaction with a panel in preview, there is no interaction context to use in a preview. -To work around this, {kib} provides a sample interaction that relies on a picked <>. +To work around this, {kib} provides a sample interaction that relies on a trigger. So in a preview, you might notice that `{{event.value}}` is replaced with `{{event.value}}` instead of with a sample from your data. Such previews can help you make sure that the structure of your URL template is valid. However, to ensure that the configured URL drilldown works as expected with your data, you have to save the dashboard and test in the panel. @@ -340,14 +252,3 @@ Tip: Consider using <> helper for date formatting. | Aggregation field behind the selected range, if available. |=== - -[float] -[[disable]] -==== Disable URL drilldown - -You can disable URL drilldown feature on your {kib} instance by disabling the plugin: - -["source","yml"] ------------ -url_drilldown.enabled: false ------------ diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 0815df4b9b0c7..6af14734444d1 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -39,9 +39,9 @@ export class DocLinksService { dashboard: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, - drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#trigger-picker`, - urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#templating`, - urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#variables`, + drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldown`, + urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, + urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#variables`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, From 2332c8b6d8eaae7c2bc96dfb1b2028286604b051 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 10 Nov 2020 08:25:32 -0700 Subject: [PATCH 14/19] [Maps] show icon when layer is filtered by time and allow layers to ignore global time range (#83006) * [Maps] show icon when layer is filtered by time and allow layers to ignore global time range * show icon if layer is narrowed by time fitler * tslint * apply global time to source check box * apply global time to join check box * tslint and jest expect updates * one more tslint fix * tslint, fix apm jest test, update time filter icon when disabling applyGlobalTime Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__tests__/__mocks__/regions_layer.mock.ts | 6 +- .../VisitorBreakdownMap/useLayerList.ts | 4 +- .../data_request_descriptor_types.ts | 10 +- .../source_descriptor_types.ts | 3 +- .../create_choropleth_layer_descriptor.ts | 2 + .../create_region_map_layer_descriptor.ts | 2 + .../maps/public/classes/layers/layer.test.ts | 2 + .../maps/public/classes/layers/layer.tsx | 83 +----- .../create_layer_descriptor.test.ts | 4 + .../observability/create_layer_descriptor.ts | 2 + .../security/create_layer_descriptors.test.ts | 6 + .../layers/vector_layer/vector_layer.tsx | 19 ++ .../es_agg_source/es_agg_source.test.ts | 12 +- .../es_geo_grid_source.test.ts | 1 + .../es_search_source/es_search_source.test.ts | 1 + .../classes/sources/es_source/es_source.ts | 22 +- .../sources/es_term_source/es_term_source.ts | 18 +- .../kibana_regionmap_source.ts | 4 - .../mvt_single_layer_vector_source.tsx | 4 - .../maps/public/classes/sources/source.ts | 7 +- .../sources/vector_source/vector_source.tsx | 2 +- .../public/classes/util/can_skip_fetch.ts | 7 +- .../components/global_time_checkbox.tsx | 32 ++ .../filter_editor/filter_editor.js | 29 +- .../layer_panel/join_editor/join_editor.tsx | 1 + .../layer_panel/join_editor/resources/join.js | 71 +++-- .../toc_entry_actions_popover.test.tsx.snap | 281 +++++++----------- .../toc_entry_actions_popover/index.ts | 10 +- .../toc_entry_actions_popover.test.tsx | 15 - .../toc_entry_actions_popover.tsx | 65 +--- .../toc_entry/toc_entry_button/index.ts | 20 ++ .../toc_entry_button/toc_entry_button.tsx | 184 ++++++++++++ 32 files changed, 553 insertions(+), 376 deletions(-) create mode 100644 x-pack/plugins/maps/public/components/global_time_checkbox.tsx create mode 100644 x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/index.ts create mode 100644 x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts index e564332583375..6d259a5a2e48c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts @@ -21,6 +21,8 @@ export const mockLayerList = [ { leftField: 'iso2', right: { + applyGlobalQuery: true, + applyGlobalTime: true, type: 'ES_TERM_SOURCE', id: '3657625d-17b0-41ef-99ba-3a2b2938655c', indexPatternTitle: 'apm-*', @@ -38,7 +40,6 @@ export const mockLayerList = [ }, ], indexPatternId: 'apm_static_index_pattern_id', - applyGlobalQuery: true, }, }, ], @@ -46,7 +47,6 @@ export const mockLayerList = [ type: 'EMS_FILE', id: 'world_countries', tooltipProperties: ['name'], - applyGlobalQuery: true, }, style: { type: 'VECTOR', @@ -96,6 +96,8 @@ export const mockLayerList = [ { leftField: 'region_iso_code', right: { + applyGlobalQuery: true, + applyGlobalTime: true, type: 'ES_TERM_SOURCE', id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', indexPatternTitle: 'apm-*', diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index bc45d58329f49..a1cdf7bb646e5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -43,6 +43,7 @@ const ES_TERM_SOURCE_COUNTRY: ESTermSourceDescriptor = { ], indexPatternId: APM_STATIC_INDEX_PATTERN_ID, applyGlobalQuery: true, + applyGlobalTime: true, }; const ES_TERM_SOURCE_REGION: ESTermSourceDescriptor = { @@ -56,6 +57,8 @@ const ES_TERM_SOURCE_REGION: ESTermSourceDescriptor = { language: 'kuery', }, indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + applyGlobalQuery: true, + applyGlobalTime: true, }; const getWhereQuery = (serviceName: string) => { @@ -158,7 +161,6 @@ export function useLayerList() { type: 'EMS_FILE', id: 'world_countries', tooltipProperties: [COUNTRY_NAME], - applyGlobalQuery: true, }, style: getLayerStyle(TRANSACTION_DURATION_COUNTRY), id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index fc691f339f34a..68fc784182a77 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -38,17 +38,17 @@ export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncM export type VectorSourceRequestMeta = MapFilters & { applyGlobalQuery: boolean; + applyGlobalTime: boolean; fieldNames: string[]; geogridPrecision?: number; sourceQuery?: MapQuery; sourceMeta: VectorSourceSyncMeta; }; -export type VectorJoinSourceRequestMeta = MapFilters & { - applyGlobalQuery: boolean; - fieldNames: string[]; - sourceQuery?: Query; -}; +export type VectorJoinSourceRequestMeta = Omit< + VectorSourceRequestMeta, + 'geogridPrecision' | 'sourceMeta' +> & { sourceQuery?: Query }; export type VectorStyleRequestMeta = MapFilters & { dynamicStyleFields: string[]; diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index 3dc90a12513fd..a6afbe4d55f9b 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -18,7 +18,6 @@ export type AttributionDescriptor = { export type AbstractSourceDescriptor = { id?: string; type: string; - applyGlobalQuery?: boolean; }; export type EMSTMSSourceDescriptor = AbstractSourceDescriptor & { @@ -37,6 +36,8 @@ export type AbstractESSourceDescriptor = AbstractSourceDescriptor & { id: string; indexPatternId: string; geoField?: string; + applyGlobalQuery: boolean; + applyGlobalTime: boolean; }; export type AggDescriptor = { diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index 61fb6ef54c207..43d1d39c170c0 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -61,6 +61,8 @@ function createChoroplethLayerDescriptor({ indexPatternTitle: rightIndexPatternTitle, term: rightTermField, metrics: [metricsDescriptor], + applyGlobalQuery: true, + applyGlobalTime: true, }, }, ], diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 8bf078806cfbc..8830831b8b656 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -87,6 +87,8 @@ export function createRegionMapLayerDescriptor({ indexPatternTitle: indexPatternTitle ? indexPatternTitle : indexPatternId, term: termsFieldName, metrics: [metricsDescriptor], + applyGlobalQuery: true, + applyGlobalTime: true, }, }, ], diff --git a/x-pack/plugins/maps/public/classes/layers/layer.test.ts b/x-pack/plugins/maps/public/classes/layers/layer.test.ts index 76df7c2af840a..e669ddf13e5ac 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer.test.ts @@ -74,6 +74,8 @@ describe('cloneDescriptor', () => { metrics: [{ type: AGG_TYPE.COUNT }], term: 'myTermField', type: 'joinSource', + applyGlobalQuery: true, + applyGlobalTime: true, }, }, ], diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index f75dae84b1723..7c76df7f6e877 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -8,9 +8,8 @@ import { Query } from 'src/plugins/data/public'; import _ from 'lodash'; import React, { ReactElement } from 'react'; -import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; import uuid from 'uuid/v4'; -import { i18n } from '@kbn/i18n'; import { FeatureCollection } from 'geojson'; import { DataRequest } from '../util/data_request'; import { @@ -49,8 +48,6 @@ export interface ILayer { supportsFitToBounds(): Promise; getAttributions(): Promise; getLabel(): string; - getCustomIconAndTooltipContent(): CustomIconAndTooltipContent; - getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent; renderLegendDetails(): ReactElement | null; showAtZoomLevel(zoom: number): boolean; getMinZoom(): number; @@ -64,6 +61,7 @@ export interface ILayer { getImmutableSourceProperties(): Promise; renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null; isLayerLoading(): boolean; + isFilteredByGlobalTime(): Promise; hasErrors(): boolean; getErrors(): string; getMbLayerIds(): string[]; @@ -93,16 +91,9 @@ export interface ILayer { getJoinsDisabledReason(): string | null; isFittable(): Promise; getLicensedFeatures(): Promise; + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent; } -export type Footnote = { - icon: ReactElement; - message?: string | null; -}; -export type IconAndTooltipContent = { - icon?: ReactElement | null; - tooltipContent?: string | null; - footnotes: Footnote[]; -}; + export type CustomIconAndTooltipContent = { icon: ReactElement | null; tooltipContent?: string | null; @@ -237,6 +228,10 @@ export class AbstractLayer implements ILayer { return (await this.supportsFitToBounds()) && this.isVisible(); } + async isFilteredByGlobalTime(): Promise { + return false; + } + async getDisplayName(source?: ISource): Promise { if (this._descriptor.label) { return this._descriptor.label; @@ -277,68 +272,6 @@ export class AbstractLayer implements ILayer { }; } - getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent { - let icon; - let tooltipContent = null; - const footnotes = []; - if (this.hasErrors()) { - icon = ( - - ); - tooltipContent = this.getErrors(); - } else if (this.isLayerLoading()) { - icon = ; - } else if (!this.isVisible()) { - icon = ; - tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { - defaultMessage: `Layer is hidden.`, - }); - } else if (!this.showAtZoomLevel(zoomLevel)) { - const minZoom = this.getMinZoom(); - const maxZoom = this.getMaxZoom(); - icon = ; - tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { - defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, - values: { minZoom, maxZoom }, - }); - } else { - const customIconAndTooltipContent = this.getCustomIconAndTooltipContent(); - if (customIconAndTooltipContent) { - icon = customIconAndTooltipContent.icon; - if (!customIconAndTooltipContent.areResultsTrimmed) { - tooltipContent = customIconAndTooltipContent.tooltipContent; - } else { - footnotes.push({ - icon: , - message: customIconAndTooltipContent.tooltipContent, - }); - } - } - - if (isUsingSearch && this.getQueryableIndexPatternIds().length) { - footnotes.push({ - icon: , - message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { - defaultMessage: 'Results narrowed by search bar', - }), - }); - } - } - - return { - icon, - tooltipContent, - footnotes, - }; - } - async hasLegendDetails(): Promise { return false; } diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts index 66eba3a539801..e2678ee218b4b 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts @@ -39,6 +39,8 @@ describe('createLayerDescriptor', () => { { leftField: 'iso2', right: { + applyGlobalQuery: true, + applyGlobalTime: true, id: '12345', indexPatternId: 'apm_static_index_pattern_id', indexPatternTitle: 'apm-*', @@ -176,6 +178,7 @@ describe('createLayerDescriptor', () => { }, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, geoField: 'client.geo.location', id: '12345', indexPatternId: 'apm_static_index_pattern_id', @@ -218,6 +221,7 @@ describe('createLayerDescriptor', () => { }, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, geoField: 'client.geo.location', id: '12345', indexPatternId: 'apm_static_index_pattern_id', diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts index bdd86d78b5300..5dbf07ed2a535 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts @@ -177,6 +177,8 @@ export function createLayerDescriptor({ term: 'client.geo.country_iso_code', metrics: [metricsDescriptor], whereQuery: apmSourceQuery, + applyGlobalQuery: true, + applyGlobalTime: true, }, }, ], diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts index 22456527491eb..fe04678deacae 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts @@ -33,6 +33,7 @@ describe('createLayerDescriptor', () => { minZoom: 0, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, filterByMapBounds: true, geoField: 'client.geo.location', id: '12345', @@ -140,6 +141,7 @@ describe('createLayerDescriptor', () => { minZoom: 0, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, filterByMapBounds: true, geoField: 'server.geo.location', id: '12345', @@ -247,6 +249,7 @@ describe('createLayerDescriptor', () => { minZoom: 0, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, destGeoField: 'server.geo.location', id: '12345', indexPatternId: 'id', @@ -366,6 +369,7 @@ describe('createLayerDescriptor', () => { minZoom: 0, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, filterByMapBounds: true, geoField: 'source.geo.location', id: '12345', @@ -473,6 +477,7 @@ describe('createLayerDescriptor', () => { minZoom: 0, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, filterByMapBounds: true, geoField: 'destination.geo.location', id: '12345', @@ -580,6 +585,7 @@ describe('createLayerDescriptor', () => { minZoom: 0, sourceDescriptor: { applyGlobalQuery: true, + applyGlobalTime: true, destGeoField: 'destination.geo.location', id: '12345', indexPatternId: 'id', diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index b9d7834896245..b4c0098bb1338 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -254,6 +254,7 @@ export class VectorLayer extends AbstractLayer { timeFilters: searchFilters.timeFilters, filters: searchFilters.filters, applyGlobalQuery: searchFilters.applyGlobalQuery, + applyGlobalTime: searchFilters.applyGlobalTime, }; let bounds = null; @@ -315,6 +316,22 @@ export class VectorLayer extends AbstractLayer { return indexPatternIds; } + async isFilteredByGlobalTime(): Promise { + if (this.getSource().getApplyGlobalTime() && (await this.getSource().isTimeAware())) { + return true; + } + + const joinPromises = this.getValidJoins().map(async (join) => { + return ( + join.getRightJoinSource().getApplyGlobalTime() && + (await join.getRightJoinSource().isTimeAware()) + ); + }); + return (await Promise.all(joinPromises)).some((isJoinTimeAware: boolean) => { + return isJoinTimeAware; + }); + } + async _syncJoin({ join, startLoading, @@ -331,6 +348,7 @@ export class VectorLayer extends AbstractLayer { fieldNames: joinSource.getFieldNames(), sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), + applyGlobalTime: joinSource.getApplyGlobalTime(), }; const prevDataRequest = this.getDataRequest(sourceDataId); @@ -403,6 +421,7 @@ export class VectorLayer extends AbstractLayer { geogridPrecision: source.getGeoGridPrecision(dataFilters.zoom), sourceQuery: sourceQuery ? sourceQuery : undefined, applyGlobalQuery: source.getApplyGlobalQuery(), + applyGlobalTime: source.getApplyGlobalTime(), sourceMeta: source.getSyncMeta(), }; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts index cf0170ab7f1bd..d31e8366e4ef4 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts @@ -31,7 +31,17 @@ const metricExamples = [ class TestESAggSource extends AbstractESAggSource { constructor(metrics: AggDescriptor[]) { - super({ type: 'test', id: 'foobar', indexPatternId: 'foobarid', metrics }, []); + super( + { + type: 'test', + id: 'foobar', + indexPatternId: 'foobarid', + metrics, + applyGlobalQuery: true, + applyGlobalTime: true, + }, + [] + ); } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 3b1cf3293c0d3..8ac014c820ace 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -151,6 +151,7 @@ describe('ESGeoGridSource', () => { }, extent, applyGlobalQuery: true, + applyGlobalTime: true, fieldNames: [], buffer: extent, sourceQuery: { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts index e7099115ffe5e..ec14a80ae761e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -103,6 +103,7 @@ describe('ESSearchSource', () => { }, sourceMeta: null, applyGlobalQuery: true, + applyGlobalTime: true, }; it('Should only include required props', async () => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 68b6b131978ea..bef0b8c6ea7af 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -82,8 +82,9 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource type: isValidStringConfig(descriptor.type) ? descriptor.type! : '', indexPatternId: descriptor.indexPatternId!, applyGlobalQuery: - // backfill old _.get usage - typeof descriptor.applyGlobalQuery !== 'undefined' ? !!descriptor.applyGlobalQuery : true, + typeof descriptor.applyGlobalQuery !== 'undefined' ? descriptor.applyGlobalQuery : true, + applyGlobalTime: + typeof descriptor.applyGlobalTime !== 'undefined' ? descriptor.applyGlobalTime : true, }; } @@ -96,6 +97,14 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource return this._descriptor.id; } + getApplyGlobalQuery(): boolean { + return this._descriptor.applyGlobalQuery; + } + + getApplyGlobalTime(): boolean { + return this._descriptor.applyGlobalTime; + } + isFieldAware(): boolean { return true; } @@ -203,10 +212,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource initialSearchContext?: object ): Promise { const indexPattern = await this.getIndexPattern(); - const isTimeAware = await this.isTimeAware(); - const applyGlobalQuery = - typeof searchFilters.applyGlobalQuery === 'boolean' ? searchFilters.applyGlobalQuery : true; - const globalFilters: Filter[] = applyGlobalQuery ? searchFilters.filters : []; + const globalFilters: Filter[] = searchFilters.applyGlobalQuery ? searchFilters.filters : []; const allFilters: Filter[] = [...globalFilters]; if (this.isFilterByMapBounds() && 'buffer' in searchFilters && searchFilters.buffer) { // buffer can be empty @@ -226,7 +232,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource // @ts-expect-error allFilters.push(extentFilter); } - if (isTimeAware) { + if (searchFilters.applyGlobalTime && (await this.isTimeAware())) { const filter = getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters); if (filter) { allFilters.push(filter); @@ -238,7 +244,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource searchSource.setField('index', indexPattern); searchSource.setField('size', limit); searchSource.setField('filter', allFilters); - if (applyGlobalQuery) { + if (searchFilters.applyGlobalQuery) { searchSource.setField('query', searchFilters.query); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index 3220253436168..8ef50a1cb7a1c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -28,6 +28,7 @@ import { } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; import { PropertiesMap } from '../../../../common/elasticsearch_util'; +import { isValidStringConfig } from '../../util/valid_string_config'; const TERMS_AGG_NAME = 'join'; const TERMS_BUCKET_KEYS_TO_IGNORE = ['key', 'doc_count']; @@ -48,12 +49,25 @@ export function extractPropertiesMap(rawEsData: any, countPropertyName: string): export class ESTermSource extends AbstractESAggSource { static type = SOURCE_TYPES.ES_TERM_SOURCE; + static createDescriptor(descriptor: Partial): ESTermSourceDescriptor { + const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor); + if (!isValidStringConfig(descriptor.term)) { + throw new Error('Cannot create an ESTermSource without a term'); + } + return { + ...normalizedDescriptor, + term: descriptor.term!, + type: SOURCE_TYPES.ES_TERM_SOURCE, + }; + } + private readonly _termField: ESDocField; readonly _descriptor: ESTermSourceDescriptor; constructor(descriptor: ESTermSourceDescriptor, inspectorAdapters: Adapters) { - super(AbstractESAggSource.createDescriptor(descriptor), inspectorAdapters); - this._descriptor = descriptor; + const sourceDescriptor = ESTermSource.createDescriptor(descriptor); + super(sourceDescriptor, inspectorAdapters); + this._descriptor = sourceDescriptor; this._termField = new ESDocField({ fieldName: this._descriptor.term, source: this, diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts index bf39d78a4784f..e6db127e45116 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts @@ -102,10 +102,6 @@ export class KibanaRegionmapSource extends AbstractVectorSource { return this._descriptor.name; } - async isTimeAware() { - return false; - } - canFormatFeatureProperties() { return true; } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index 6390626b006b4..adc478c1f45d3 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -189,10 +189,6 @@ export class MVTSingleLayerVectorSource return null; } - getApplyGlobalQuery(): boolean { - return false; - } - isBoundsAware() { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index f24ec012836b6..7a52c4e6fde20 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -60,6 +60,7 @@ export interface ISource { cloneDescriptor(): AbstractSourceDescriptor; getFieldNames(): string[]; getApplyGlobalQuery(): boolean; + getApplyGlobalTime(): boolean; getIndexPatternIds(): string[]; getQueryableIndexPatternIds(): string[]; getGeoGridPrecision(zoom: number): number; @@ -135,7 +136,11 @@ export class AbstractSource implements ISource { } getApplyGlobalQuery(): boolean { - return !!this._descriptor.applyGlobalQuery; + return false; + } + + getApplyGlobalTime(): boolean { + return false; } getIndexPatternIds(): string[] { diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 38ff3b49a87f4..32db97708e397 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -37,6 +37,7 @@ export interface GeoJsonWithMeta { export interface BoundsFilters { applyGlobalQuery: boolean; + applyGlobalTime: boolean; filters: Filter[]; query?: MapQuery; sourceQuery?: MapQuery; @@ -61,7 +62,6 @@ export interface IVectorSource extends ISource { getLeftJoinFields(): Promise; getSyncMeta(): VectorSourceSyncMeta | null; getFieldNames(): string[]; - getApplyGlobalQuery(): boolean; createField({ fieldName }: { fieldName: string }): IField; canFormatFeatureProperties(): boolean; getSupportedShapeTypes(): Promise; diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 147870dbef371..a7919ad058e4b 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -84,9 +84,13 @@ export async function canSkipSourceUpdate({ return false; } + let updateDueToApplyGlobalTime = false; let updateDueToTime = false; if (timeAware) { - updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + updateDueToApplyGlobalTime = prevMeta.applyGlobalTime !== nextMeta.applyGlobalTime; + if (nextMeta.applyGlobalTime) { + updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + } } let updateDueToRefreshTimer = false; @@ -132,6 +136,7 @@ export async function canSkipSourceUpdate({ const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); return ( + !updateDueToApplyGlobalTime && !updateDueToTime && !updateDueToRefreshTimer && !updateDueToExtentChange && diff --git a/x-pack/plugins/maps/public/components/global_time_checkbox.tsx b/x-pack/plugins/maps/public/components/global_time_checkbox.tsx new file mode 100644 index 0000000000000..b224ad6474745 --- /dev/null +++ b/x-pack/plugins/maps/public/components/global_time_checkbox.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; + +interface Props { + applyGlobalTime: boolean; + label: string; + setApplyGlobalTime: (applyGlobalTime: boolean) => void; +} + +export function GlobalTimeCheckbox({ applyGlobalTime, label, setApplyGlobalTime }: Props) { + const onApplyGlobalTimeChange = (event: EuiSwitchEvent) => { + setApplyGlobalTime(event.target.checked); + }; + + return ( + + + + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index d2652fac5bd2c..70a04ec37860d 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -22,23 +22,26 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getIndexPatternService, getData } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; +import { GlobalTimeCheckbox } from '../../../components/global_time_checkbox'; export class FilterEditor extends Component { state = { isPopoverOpen: false, indexPatterns: [], + isSourceTimeAware: false, }; componentDidMount() { this._isMounted = true; this._loadIndexPatterns(); + this._loadSourceTimeAware(); } componentWillUnmount() { this._isMounted = false; } - _loadIndexPatterns = async () => { + async _loadIndexPatterns() { // Filter only effects source so only load source indices. const indexPatternIds = this.props.layer.getSource().getIndexPatternIds(); const indexPatterns = []; @@ -58,7 +61,14 @@ export class FilterEditor extends Component { } this.setState({ indexPatterns }); - }; + } + + async _loadSourceTimeAware() { + const isSourceTimeAware = await this.props.layer.getSource().isTimeAware(); + if (this._isMounted) { + this.setState({ isSourceTimeAware }); + } + } _toggle = () => { this.setState((prevState) => ({ @@ -79,6 +89,10 @@ export class FilterEditor extends Component { this.props.updateSourceProp(this.props.layer.getId(), 'applyGlobalQuery', applyGlobalQuery); }; + _onApplyGlobalTimeChange = (applyGlobalTime) => { + this.props.updateSourceProp(this.props.layer.getId(), 'applyGlobalTime', applyGlobalTime); + }; + _renderQueryPopover() { const layerQuery = this.props.layer.getQuery(); const { SearchBar } = getData().ui; @@ -165,6 +179,15 @@ export class FilterEditor extends Component { } render() { + const globalTimeCheckbox = this.state.isSourceTimeAware ? ( + + ) : null; return ( @@ -191,6 +214,8 @@ export class FilterEditor extends Component { applyGlobalQuery={this.props.layer.getSource().getApplyGlobalQuery()} setApplyGlobalQuery={this._onApplyGlobalQueryChange} /> + + {globalTimeCheckbox} ); } diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx index 2065668858e22..2d14ba20047bc 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx @@ -66,6 +66,7 @@ export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDispla right: { id: uuid(), applyGlobalQuery: true, + applyGlobalTime: true, }, } as JoinDescriptor, ]); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js index 5b44801eb780d..54c2b908d1536 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js @@ -12,6 +12,7 @@ import { JoinExpression } from './join_expression'; import { MetricsExpression } from './metrics_expression'; import { WhereExpression } from './where_expression'; import { GlobalFilterCheckbox } from '../../../../components/global_filter_checkbox'; +import { GlobalTimeCheckbox } from '../../../../components/global_time_checkbox'; import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; @@ -126,6 +127,16 @@ export class Join extends Component { }); }; + _onApplyGlobalTimeChange = (applyGlobalTime) => { + this.props.onChange({ + leftField: this.props.join.leftField, + right: { + ...this.props.join.right, + applyGlobalTime, + }, + }); + }; + render() { const { join, onRemove, leftFields, leftSourceName } = this.props; const { rightFields, indexPattern } = this.state; @@ -137,6 +148,7 @@ export class Join extends Component { let metricsExpression; let globalFilterCheckbox; + let globalTimeCheckbox; if (isJoinConfigComplete) { metricsExpression = ( @@ -148,16 +160,27 @@ export class Join extends Component { ); globalFilterCheckbox = ( - - + ); + if (this.state.indexPattern && this.state.indexPattern.timeFieldName) { + globalTimeCheckbox = ( + - - ); + ); + } } let whereExpression; @@ -194,22 +217,24 @@ export class Join extends Component { {metricsExpression} {whereExpression} - - {globalFilterCheckbox} - - + + {globalFilterCheckbox} + + {globalTimeCheckbox} + +
); } diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index 456414889c732..ea37e76bc8494 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -5,49 +5,35 @@ exports[`TOCEntryActionsPopover is rendered 1`] = ` anchorClassName="mapLayTocActions__popoverAnchor" anchorPosition="leftUp" button={ - - simulated tooltip content at zoom: 0 -
- - mockFootnoteIcon - - - simulated footnote at isUsingSearch: true -
- + - - - - mockIcon - - - layer 1 - - - - - mockFootnoteIcon - - - -
+ onClick={[Function]} + /> } className="mapLayTocActions" closePopover={[Function]} @@ -132,49 +118,35 @@ exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBou anchorClassName="mapLayTocActions__popoverAnchor" anchorPosition="leftUp" button={ - - simulated tooltip content at zoom: 0 -
- - mockFootnoteIcon - - - simulated footnote at isUsingSearch: true -
- + - - - - mockIcon - - - layer 1 - - - - - mockFootnoteIcon - - - -
+ onClick={[Function]} + /> } className="mapLayTocActions" closePopover={[Function]} @@ -259,49 +231,36 @@ exports[`TOCEntryActionsPopover should have "show layer" action when layer is no anchorClassName="mapLayTocActions__popoverAnchor" anchorPosition="leftUp" button={ - - simulated tooltip content at zoom: 0 -
- - mockFootnoteIcon - - - simulated footnote at isUsingSearch: true -
- + - - - - mockIcon - - - layer 1 - - - - - mockFootnoteIcon - - - -
+ onClick={[Function]} + /> } className="mapLayTocActions" closePopover={[Function]} @@ -386,49 +345,35 @@ exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1 anchorClassName="mapLayTocActions__popoverAnchor" anchorPosition="leftUp" button={ - - simulated tooltip content at zoom: 0 -
- - mockFootnoteIcon - - - simulated footnote at isUsingSearch: true -
- + - - - - mockIcon - - - layer 1 - - - - - mockFootnoteIcon - - - -
+ onClick={[Function]} + /> } className="mapLayTocActions" closePopover={[Function]} diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts index d8d43e1e1b27a..673bebc43582e 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts @@ -14,15 +14,12 @@ import { cloneLayer, removeLayer, } from '../../../../../../actions'; -import { getMapZoom, isUsingSearch } from '../../../../../../selectors/map_selectors'; import { getIsReadOnly } from '../../../../../../selectors/ui_selectors'; import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; function mapStateToProps(state: MapStoreState) { return { isReadOnly: getIsReadOnly(state), - isUsingSearch: isUsingSearch(state), - zoom: getMapZoom(state), }; } @@ -43,8 +40,5 @@ function mapDispatchToProps(dispatch: ThunkDispatchmockIcon, - tooltipContent: `simulated tooltip content at zoom: ${zoom}`, - footnotes: [ - { - icon: mockFootnoteIcon, - message: `simulated footnote at isUsingSearch: ${isUsingSearch}`, - }, - ], - }; - } } const defaultProps = { @@ -59,11 +46,9 @@ const defaultProps = { fitToBounds: () => {}, isEditButtonDisabled: false, isReadOnly: false, - isUsingSearch: true, layer: new LayerMock(), removeLayer: () => {}, toggleVisible: () => {}, - zoom: 0, }; describe('TOCEntryActionsPopover', () => { diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index b46de57e6a789..4d669dfbe235e 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; -import { EuiButtonEmpty, EuiPopover, EuiContextMenu, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { EuiPopover, EuiContextMenu, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ILayer } from '../../../../../../classes/layers/layer'; +import { TOCEntryButton } from '../toc_entry_button'; interface Props { cloneLayer: (layerId: string) => void; @@ -18,11 +19,9 @@ interface Props { fitToBounds: (layerId: string) => void; isEditButtonDisabled: boolean; isReadOnly: boolean; - isUsingSearch: boolean; layer: ILayer; removeLayer: (layerId: string) => void; toggleVisible: (layerId: string) => void; - zoom: number; } interface State { @@ -82,55 +81,6 @@ export class TOCEntryActionsPopover extends Component { this.props.toggleVisible(this.props.layer.getId()); } - _renderPopoverToggleButton() { - const { icon, tooltipContent, footnotes } = this.props.layer.getIconAndTooltipContent( - this.props.zoom, - this.props.isUsingSearch - ); - - const footnoteIcons = footnotes.map((footnote, index) => { - return ( - - {''} - {footnote.icon} - - ); - }); - const footnoteTooltipContent = footnotes.map((footnote, index) => { - return ( -
- {footnote.icon} {footnote.message} -
- ); - }); - - return ( - - {tooltipContent} - {footnoteTooltipContent} - - } - > - - {icon} - {this.props.displayName} {footnoteIcons} - - - ); - } - _getActionsPanel() { const actionItems = [ { @@ -222,7 +172,14 @@ export class TOCEntryActionsPopover extends Component { + } isOpen={this.state.isPopoverOpen} closePopover={this._closePopover} panelPaddingSize="none" diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/index.ts b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/index.ts new file mode 100644 index 0000000000000..d83c158d0385f --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { MapStoreState } from '../../../../../../reducers/store'; +import { getMapZoom, isUsingSearch } from '../../../../../../selectors/map_selectors'; +import { TOCEntryButton, StateProps, OwnProps } from './toc_entry_button'; + +function mapStateToProps(state: MapStoreState, ownProps: OwnProps): StateProps { + return { + isUsingSearch: isUsingSearch(state), + zoom: getMapZoom(state), + }; +} + +const connected = connect(mapStateToProps)(TOCEntryButton); +export { connected as TOCEntryButton }; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx new file mode 100644 index 0000000000000..87c60278b361a --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx @@ -0,0 +1,184 @@ +/* + * 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, { Component, Fragment, ReactElement } from 'react'; + +import { EuiButtonEmpty, EuiIcon, EuiToolTip, EuiLoadingSpinner } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ILayer } from '../../../../../../classes/layers/layer'; + +interface Footnote { + icon: ReactElement; + message?: string | null; +} + +interface IconAndTooltipContent { + icon?: ReactElement | null; + tooltipContent?: string | null; + footnotes: Footnote[]; +} + +export interface StateProps { + isUsingSearch: boolean; + zoom: number; +} + +export interface OwnProps { + displayName: string; + escapedDisplayName: string; + layer: ILayer; + onClick: () => void; +} + +type Props = StateProps & OwnProps; + +interface State { + isFilteredByGlobalTime: boolean; +} + +export class TOCEntryButton extends Component { + private _isMounted: boolean = false; + + state: State = { + isFilteredByGlobalTime: false, + }; + + componentDidMount() { + this._isMounted = true; + this._loadIsFilteredByGlobalTime(); + } + + componentDidUpdate() { + this._loadIsFilteredByGlobalTime(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadIsFilteredByGlobalTime() { + const isFilteredByGlobalTime = await this.props.layer.isFilteredByGlobalTime(); + if (this._isMounted && isFilteredByGlobalTime !== this.state.isFilteredByGlobalTime) { + this.setState({ isFilteredByGlobalTime }); + } + } + + getIconAndTooltipContent(): IconAndTooltipContent { + let icon; + let tooltipContent = null; + const footnotes = []; + if (this.props.layer.hasErrors()) { + icon = ( + + ); + tooltipContent = this.props.layer.getErrors(); + } else if (this.props.layer.isLayerLoading()) { + icon = ; + } else if (!this.props.layer.isVisible()) { + icon = ; + tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { + defaultMessage: `Layer is hidden.`, + }); + } else if (!this.props.layer.showAtZoomLevel(this.props.zoom)) { + const minZoom = this.props.layer.getMinZoom(); + const maxZoom = this.props.layer.getMaxZoom(); + icon = ; + tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { + defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, + values: { minZoom, maxZoom }, + }); + } else { + const customIconAndTooltipContent = this.props.layer.getCustomIconAndTooltipContent(); + if (customIconAndTooltipContent) { + icon = customIconAndTooltipContent.icon; + if (!customIconAndTooltipContent.areResultsTrimmed) { + tooltipContent = customIconAndTooltipContent.tooltipContent; + } else { + footnotes.push({ + icon: , + message: customIconAndTooltipContent.tooltipContent, + }); + } + } + + if (this.props.isUsingSearch && this.props.layer.getQueryableIndexPatternIds().length) { + footnotes.push({ + icon: , + message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { + defaultMessage: 'Results narrowed by search bar', + }), + }); + } + if (this.state.isFilteredByGlobalTime) { + footnotes.push({ + icon: , + message: i18n.translate('xpack.maps.layer.isUsingTimeFilter', { + defaultMessage: 'Results narrowed by time filter', + }), + }); + } + } + + return { + icon, + tooltipContent, + footnotes, + }; + } + + render() { + const { icon, tooltipContent, footnotes } = this.getIconAndTooltipContent(); + + const footnoteIcons = footnotes.map((footnote, index) => { + return ( + + {''} + {footnote.icon} + + ); + }); + const footnoteTooltipContent = footnotes.map((footnote, index) => { + return ( +
+ {footnote.icon} {footnote.message} +
+ ); + }); + + return ( + + {tooltipContent} + {footnoteTooltipContent} + + } + > + + {icon} + {this.props.displayName} {footnoteIcons} + + + ); + } +} From 291c34c84c46a08c53143d66f6bbeb797274e80c Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Tue, 10 Nov 2020 10:25:59 -0500 Subject: [PATCH 15/19] [App Search] Added the log retention panel to the Settings page (#82982) * Added LogRententionPanel * i18n * fix axe failure * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts Co-authored-by: Constance * Apply suggestions from code review Co-authored-by: Constance * PR Feeback: interpolation * Apply suggestions from code review Co-authored-by: Constance Co-authored-by: Constance --- .../log_retention_panel.test.tsx | 187 ++++++++++++++++++ .../log_retention/log_retention_panel.tsx | 110 +++++++++++ .../log_retention/messaging/constants.ts | 119 +++++++++++ .../determine_tooltip_content.test.ts | 168 ++++++++++++++++ .../messaging/determine_tooltip_content.ts | 46 +++++ .../log_retention/messaging/index.test.tsx | 74 +++++++ .../log_retention/messaging/index.tsx | 46 +++++ .../settings/log_retention/messaging/types.ts | 18 ++ .../components/settings/settings.tsx | 19 +- 9 files changed, 782 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx new file mode 100644 index 0000000000000..b4f7e92fac8cc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.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 '../../../../__mocks__/kea.mock'; +import '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { LogRetentionPanel } from './log_retention_panel'; +import { ILogRetention } from './types'; + +describe('', () => { + const actions = { + fetchLogRetention: jest.fn(), + toggleLogRetention: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders', () => { + const logRetentionPanel = shallow(); + expect(logRetentionPanel.find('[data-test-subj="LogRetentionPanel"]')).toHaveLength(1); + }); + + it('initializes data on mount', () => { + shallow(); + expect(actions.fetchLogRetention).toHaveBeenCalledTimes(1); + }); + + it('renders Analytics switch off when analytics log retention is false in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: false, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('checked') + ).toEqual(false); + }); + + it('renders Analytics switch on when analyticsLogRetention is true in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: true, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('checked') + ).toEqual(true); + }); + + it('renders API switch off when apiLogRetention is false in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + api: { + enabled: false, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('checked') + ).toEqual(false); + }); + + it('renders API switch on when apiLogRetention is true in LogRetentionLogic ', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + api: { + enabled: true, + }, + }), + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('checked') + ).toEqual(true); + }); + + it('enables both switches when isLogRetentionUpdating is false', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({}), + }); + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('disabled') + ).toEqual(false); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('disabled') + ).toEqual(false); + }); + + it('disables both switches when isLogRetentionUpdating is true', () => { + setMockValues({ + isLogRetentionUpdating: true, + logRetention: mockLogRetention({}), + }); + const logRetentionPanel = shallow(); + + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]').prop('disabled') + ).toEqual(true); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('disabled') + ).toEqual(true); + }); + + it('calls toggleLogRetention when analytics log retention option is changed', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: false, + }, + }), + }); + const logRetentionPanel = shallow(); + logRetentionPanel + .find('[data-test-subj="LogRetentionPanelAnalyticsSwitch"]') + .simulate('change'); + expect(actions.toggleLogRetention).toHaveBeenCalledWith('analytics'); + }); + + it('calls toggleLogRetention when api log retention option is changed', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + analytics: { + enabled: false, + }, + }), + }); + const logRetentionPanel = shallow(); + logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').simulate('change'); + expect(actions.toggleLogRetention).toHaveBeenCalledWith('api'); + }); +}); + +const mockLogRetention = (logRetention: Partial) => { + const baseLogRetention = { + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }; + + return { + analytics: { + ...baseLogRetention.analytics, + ...logRetention.analytics, + }, + api: { + ...baseLogRetention.api, + ...logRetention.api, + }, + }; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx new file mode 100644 index 0000000000000..7b8eea061d110 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; +import { useActions, useValues } from 'kea'; + +import { LogRetentionLogic } from './log_retention_logic'; +import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage } from './messaging'; +import { ELogRetentionOptions } from './types'; + +export const LogRetentionPanel: React.FC = () => { + const { toggleLogRetention, fetchLogRetention } = useActions(LogRetentionLogic); + + const { logRetention, isLogRetentionUpdating } = useValues(LogRetentionLogic); + + const hasILM = logRetention !== null; + const analyticsLogRetentionSettings = logRetention?.[ELogRetentionOptions.Analytics]; + const apiLogRetentionSettings = logRetention?.[ELogRetentionOptions.API]; + + useEffect(() => { + fetchLogRetention(); + }, []); + + return ( +
+ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.title', { + defaultMessage: 'Log Retention', + })} +

+
+ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { + defaultMessage: 'Manage the default write settings for API Logs and Analytics.', + })}{' '} + + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { + defaultMessage: 'Learn more about retention settings.', + })} + +

+
+ + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label', + { + defaultMessage: 'Analytics Logs', + } + )} + + {': '} + {hasILM && ( + + + + )} + + } + checked={!!analyticsLogRetentionSettings?.enabled} + onChange={() => toggleLogRetention(ELogRetentionOptions.Analytics)} + disabled={isLogRetentionUpdating} + data-test-subj="LogRetentionPanelAnalyticsSwitch" + /> + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.label', + { + defaultMessage: 'API Logs', + } + )} + + {': '} + {hasILM && ( + + + + )} + + } + checked={!!apiLogRetentionSettings?.enabled} + onChange={() => toggleLogRetention(ELogRetentionOptions.API)} + disabled={isLogRetentionUpdating} + data-test-subj="LogRetentionPanelAPISwitch" + /> + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts new file mode 100644 index 0000000000000..f7f0fbdff1acb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts @@ -0,0 +1,119 @@ +/* + * 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 { ILogRetentionMessages } from './types'; +import { renderLogRetentionDate } from '.'; + +const ANALYTICS_NO_LOGGING = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging', + { + defaultMessage: 'Analytics collection has been disabled for all engines.', + } +); + +const ANALYTICS_NO_LOGGING_COLLECTED = (disabledAt: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.collected', + { + defaultMessage: 'The last date analytics were collected was {disabledAt}.', + values: { disabledAt }, + } + ); + +const ANALYTICS_NO_LOGGING_NOT_COLLECTED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.notCollected', + { + defaultMessage: 'There are no analytics collected.', + } +); + +const ANALYTICS_ILM_DISABLED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.ilmDisabled', + { + defaultMessage: "App Search isn't managing analytics retention.", + } +); + +const ANALYTICS_CUSTOM_POLICY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.customPolicy', + { + defaultMessage: 'You have a custom analytics retention policy.', + } +); + +const ANALYTICS_STORED = (minAgeDays: number | null | undefined) => + i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.stored', { + defaultMessage: 'Your analytics are being stored for at least {minAgeDays} days.', + values: { minAgeDays }, + }); + +const API_NO_LOGGING = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging', + { + defaultMessage: 'API logging has been disabled for all engines.', + } +); + +const API_NO_LOGGING_COLLECTED = (disabledAt: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.collected', { + defaultMessage: 'The last date logs were collected was {disabledAt}.', + values: { disabledAt }, + }); + +const API_NO_LOGGING_NOT_COLLECTED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.notCollected', + { + defaultMessage: 'There are no logs collected.', + } +); + +const API_ILM_DISABLED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.ilmDisabled', + { + defaultMessage: "App Search isn't managing API log retention.", + } +); + +const API_CUSTOM_POLICY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.customPolicy', + { + defaultMessage: 'You have a custom API log retention policy.', + } +); + +const API_STORED = (minAgeDays: number | null | undefined) => + i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.api.stored', { + defaultMessage: 'Your logs are being stored for at least {minAgeDays} days.', + values: { minAgeDays }, + }); + +export const ANALYTICS_MESSAGES: ILogRetentionMessages = { + noLogging: (_, logRetentionSettings) => + `${ANALYTICS_NO_LOGGING} ${ + logRetentionSettings.disabledAt + ? ANALYTICS_NO_LOGGING_COLLECTED(renderLogRetentionDate(logRetentionSettings.disabledAt)) + : ANALYTICS_NO_LOGGING_NOT_COLLECTED + }`, + ilmDisabled: ANALYTICS_ILM_DISABLED, + customPolicy: ANALYTICS_CUSTOM_POLICY, + defaultPolicy: (_, logRetentionSettings) => + ANALYTICS_STORED(logRetentionSettings.retentionPolicy?.minAgeDays), +}; + +export const API_MESSAGES: ILogRetentionMessages = { + noLogging: (_, logRetentionSettings) => + `${API_NO_LOGGING} ${ + logRetentionSettings.disabledAt + ? API_NO_LOGGING_COLLECTED(renderLogRetentionDate(logRetentionSettings.disabledAt)) + : API_NO_LOGGING_NOT_COLLECTED + }`, + ilmDisabled: API_ILM_DISABLED, + customPolicy: API_CUSTOM_POLICY, + defaultPolicy: (_, logRetentionSettings) => + API_STORED(logRetentionSettings.retentionPolicy?.minAgeDays), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts new file mode 100644 index 0000000000000..fbc2ccfbc8a52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts @@ -0,0 +1,168 @@ +/* + * 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 { determineTooltipContent } from './determine_tooltip_content'; +import { ANALYTICS_MESSAGES, API_MESSAGES } from './constants'; + +describe('determineTooltipContent', () => { + const BASE_SETTINGS = { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }; + + it('will return nothing if settings are not provided', () => { + expect(determineTooltipContent(ANALYTICS_MESSAGES, true)).toBeUndefined(); + }); + + describe('analytics messages', () => { + describe('when analytics logs are enabled', () => { + describe("and they're using the default policy", () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 7, + }, + }) + ).toEqual('Your analytics are being stored for at least 7 days.'); + }); + }); + + describe('and there is a custom policy', () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: false, + minAgeDays: 7, + }, + }) + ).toEqual('You have a custom analytics retention policy.'); + }); + }); + }); + + describe('when analytics logs are disabled', () => { + describe('and there is no disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: null, + }) + ).toEqual( + 'Analytics collection has been disabled for all engines. There are no analytics collected.' + ); + }); + }); + + describe('and there is a disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: 'Thu, 05 Nov 2020 18:57:28 +0000', + }) + ).toEqual( + 'Analytics collection has been disabled for all engines. The last date analytics were collected was November 5, 2020.' + ); + }); + }); + }); + + describe('when ilm is disabled entirely', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(ANALYTICS_MESSAGES, false, { + ...BASE_SETTINGS, + enabled: true, + }) + ).toEqual("App Search isn't managing analytics retention."); + }); + }); + }); + + describe('api messages', () => { + describe('when analytics logs are enabled', () => { + describe("and they're using the default policy", () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 7, + }, + }) + ).toEqual('Your logs are being stored for at least 7 days.'); + }); + }); + + describe('and there is a custom policy', () => { + it('will render a retention policy message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: true, + retentionPolicy: { + isDefault: false, + minAgeDays: 7, + }, + }) + ).toEqual('You have a custom API log retention policy.'); + }); + }); + }); + + describe('when analytics logs are disabled', () => { + describe('and there is no disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: null, + }) + ).toEqual('API logging has been disabled for all engines. There are no logs collected.'); + }); + }); + + describe('and there is a disabledAt date', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(API_MESSAGES, true, { + ...BASE_SETTINGS, + enabled: false, + disabledAt: 'Thu, 05 Nov 2020 18:57:28 +0000', + }) + ).toEqual( + 'API logging has been disabled for all engines. The last date logs were collected was November 5, 2020.' + ); + }); + }); + }); + + describe('when ilm is disabled entirely', () => { + it('will render a no logging message', () => { + expect( + determineTooltipContent(API_MESSAGES, false, { + ...BASE_SETTINGS, + enabled: true, + }) + ).toEqual("App Search isn't managing API log retention."); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts new file mode 100644 index 0000000000000..e361e28490a83 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ILogRetentionSettings } from '../types'; +import { TMessageStringOrFunction, ILogRetentionMessages } from './types'; + +export const determineTooltipContent = ( + messages: ILogRetentionMessages, + ilmEnabled: boolean, + logRetentionSettings?: ILogRetentionSettings +) => { + if (typeof logRetentionSettings === 'undefined') { + return; + } + + const renderOrReturnMessage = (message: TMessageStringOrFunction) => { + if (typeof message === 'function') { + return message(ilmEnabled, logRetentionSettings); + } + return message; + }; + + if (!logRetentionSettings.enabled) { + return renderOrReturnMessage(messages.noLogging); + } + if (logRetentionSettings.enabled && !ilmEnabled) { + return renderOrReturnMessage(messages.ilmDisabled); + } + if ( + logRetentionSettings.enabled && + ilmEnabled && + !logRetentionSettings.retentionPolicy?.isDefault + ) { + return renderOrReturnMessage(messages.customPolicy); + } + if ( + logRetentionSettings.enabled && + ilmEnabled && + logRetentionSettings.retentionPolicy?.isDefault + ) { + return renderOrReturnMessage(messages.defaultPolicy); + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx new file mode 100644 index 0000000000000..b65ffc04ad700 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 '../../../../../__mocks__/kea.mock'; +import { setMockValues } from '../../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage, renderLogRetentionDate } from '.'; + +describe('LogRetentionMessaging', () => { + const LOG_RETENTION = { + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }; + + describe('renderLogRetentionDate', () => { + it('renders a formatted date', () => { + expect(renderLogRetentionDate('Thu, 05 Nov 2020 18:57:28 +0000')).toEqual('November 5, 2020'); + }); + }); + + describe('AnalyticsLogRetentionMessage', () => { + it('renders', () => { + setMockValues({ + ilmEnabled: true, + logRetention: LOG_RETENTION, + }); + const wrapper = shallow(); + expect(wrapper.text()).toEqual('Your analytics are being stored for at least 180 days.'); + }); + + it('renders nothing if logRetention is null', () => { + setMockValues({ + ilmEnabled: true, + logRetention: null, + }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toEqual(true); + }); + }); + + describe('ApiLogRetentionMessage', () => { + it('renders', () => { + setMockValues({ + ilmEnabled: true, + logRetention: LOG_RETENTION, + }); + const wrapper = shallow(); + expect(wrapper.text()).toEqual('Your logs are being stored for at least 180 days.'); + }); + + it('renders nothing if logRetention is null', () => { + setMockValues({ + ilmEnabled: true, + logRetention: null, + }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx new file mode 100644 index 0000000000000..9fe2e8dc72ed6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { useValues } from 'kea'; +import moment from 'moment'; + +import { AppLogic } from '../../../../app_logic'; +import { LogRetentionLogic } from '../log_retention_logic'; +import { ELogRetentionOptions } from '../types'; + +import { determineTooltipContent } from './determine_tooltip_content'; +import { ANALYTICS_MESSAGES, API_MESSAGES } from './constants'; + +export const renderLogRetentionDate = (dateString: string) => + moment(dateString).format('MMMM D, YYYY'); + +export const AnalyticsLogRetentionMessage: React.FC = () => { + const { ilmEnabled } = useValues(AppLogic); + const { logRetention } = useValues(LogRetentionLogic); + if (!logRetention) return null; + + return ( + <> + {determineTooltipContent( + ANALYTICS_MESSAGES, + ilmEnabled, + logRetention[ELogRetentionOptions.Analytics] + )} + + ); +}; + +export const ApiLogRetentionMessage: React.FC = () => { + const { ilmEnabled } = useValues(AppLogic); + const { logRetention } = useValues(LogRetentionLogic); + if (!logRetention) return null; + + return ( + <>{determineTooltipContent(API_MESSAGES, ilmEnabled, logRetention[ELogRetentionOptions.API])} + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts new file mode 100644 index 0000000000000..a9546343d9aa7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts @@ -0,0 +1,18 @@ +/* + * 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 { ILogRetentionSettings } from '../types'; + +export type TMessageStringOrFunction = + | string + | ((ilmEnabled: boolean, logRetentionSettings: ILogRetentionSettings) => string); + +export interface ILogRetentionMessages { + noLogging: TMessageStringOrFunction; + ilmDisabled: TMessageStringOrFunction; + customPolicy: TMessageStringOrFunction; + defaultPolicy: TMessageStringOrFunction; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index 13079bb380f13..7bd3683919bc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -5,11 +5,17 @@ */ import React from 'react'; - -import { EuiPageHeader, EuiPageHeaderSection, EuiPageContentBody, EuiTitle } from '@elastic/eui'; +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiPageContent, + EuiPageContentBody, + EuiTitle, +} from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { LogRetentionPanel } from './log_retention/log_retention_panel'; import { SETTINGS_TITLE } from './'; @@ -24,9 +30,12 @@ export const Settings: React.FC = () => { - - - + + + + + + ); }; From 00ca555cd98b058420898498114666b443b0ccbf Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 10 Nov 2020 10:50:31 -0500 Subject: [PATCH 16/19] [Security Solution][Detections] - follow up cleanup on auto refresh rules (#83023) ### Summary This is a follow up cleanup PR to #82062 . There were a few comments I hadn't gotten to and wanted to follow up on. --- .../cypress/tasks/alerts_detection_rules.ts | 1 - .../detection_engine/rules/all/index.tsx | 19 +++++++------------ .../rules/all/utility_bar.test.tsx | 12 +++++++----- .../security_solution/server/ui_settings.ts | 17 +++++++++-------- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index d4602dcd16db8..bcee151a6be42 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -125,7 +125,6 @@ export const waitForRulesToBeLoaded = () => { cy.get(ASYNC_LOADING_PROGRESS).should('not.exist'); }; -// when using, ensure you've called cy.clock prior in test export const checkAutoRefresh = (ms: number, condition: string) => { cy.get(ASYNC_LOADING_PROGRESS).should('not.be.visible'); cy.tick(ms); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 663a4bb242c06..dcf9765c0cdd1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -13,6 +13,7 @@ import { EuiProgress, EuiOverlayMask, EuiConfirmModal, + EuiWindowEvent, } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; @@ -406,18 +407,6 @@ export const AllRules = React.memo( [setAutoRefreshOn, handleRefreshData] ); - useEffect(() => { - debounceResetIdleTimer(); - - window.addEventListener('mousemove', debounceResetIdleTimer, { passive: true }); - window.addEventListener('keydown', debounceResetIdleTimer); - - return () => { - window.removeEventListener('mousemove', debounceResetIdleTimer); - window.removeEventListener('keydown', debounceResetIdleTimer); - }; - }, [handleResetIdleTimer, debounceResetIdleTimer]); - const shouldShowRulesTable = useMemo( (): boolean => showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading, [initLoading, rulesCustomInstalled, rulesInstalled] @@ -470,6 +459,12 @@ export const AllRules = React.memo( return ( <> + + + + + + ({ eui: euiDarkVars, darkMode: true }); + describe('AllRules', () => { it('renders AllRulesUtilityBar total rules and selected rules', () => { const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> + { it('renders utility actions if user has permissions', () => { const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> + { it('renders no utility actions if user has no permissions', () => { const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> + { it('invokes refresh on refresh action click', () => { const mockRefresh = jest.fn(); const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> + { it('invokes onRefreshSwitch when auto refresh switch is clicked', async () => { const mockSwitch = jest.fn(); const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> + { '

Default refresh interval for the Security time filter, in milliseconds.

', } ), - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.object({ value: schema.number(), @@ -66,7 +67,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { description: i18n.translate('xpack.securitySolution.uiSettings.defaultTimeRangeDescription', { defaultMessage: '

Default period of time in the Security time filter.

', }), - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.object({ from: schema.string(), @@ -82,7 +83,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { defaultMessage: '

Comma-delimited list of Elasticsearch indices from which the Security app collects events.

', }), - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.arrayOf(schema.string()), }, @@ -99,7 +100,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { '

Value above which Machine Learning job anomalies are displayed in the Security app.

Valid values: 0 to 100.

', } ), - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.number(), }, @@ -112,7 +113,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { defaultMessage: '

Enables the News feed

', }), type: 'boolean', - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.boolean(), }, @@ -133,7 +134,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { "value": ${DEFAULT_RULE_REFRESH_INTERVAL_VALUE}, "idleTimeout": ${DEFAULT_RULE_REFRESH_IDLE_VALUE} }`, - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.object({ idleTimeout: schema.number({ min: 300000 }), @@ -149,7 +150,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { description: i18n.translate('xpack.securitySolution.uiSettings.newsFeedUrlDescription', { defaultMessage: '

News feed content will be retrieved from this URL

', }), - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.string(), }, @@ -166,7 +167,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', } ), - category: ['securitySolution'], + category: [APP_ID], requiresPageReload: true, schema: schema.arrayOf( schema.object({ From 4dba10c76a7f5daead0650030d84393d65f65c06 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Tue, 10 Nov 2020 09:51:27 -0600 Subject: [PATCH 17/19] Adds cloud links to user menu (#82803) Co-authored-by: Ryan Keairns --- x-pack/plugins/cloud/kibana.json | 2 +- x-pack/plugins/cloud/public/index.ts | 2 +- x-pack/plugins/cloud/public/mocks.ts | 18 +++ x-pack/plugins/cloud/public/plugin.ts | 28 +++- .../plugins/cloud/public/user_menu_links.ts | 38 +++++ x-pack/plugins/cloud/server/config.ts | 2 + x-pack/plugins/security/public/index.ts | 1 + x-pack/plugins/security/public/mocks.ts | 7 + .../security/public/nav_control/index.mock.ts | 14 ++ .../security/public/nav_control/index.ts | 3 +- .../nav_control/nav_control_component.scss | 11 ++ .../nav_control_component.test.tsx | 38 +++++ .../nav_control/nav_control_component.tsx | 139 ++++++++++++------ .../nav_control/nav_control_service.test.ts | 130 ++++++++++++++++ .../nav_control/nav_control_service.tsx | 41 +++++- .../plugins/security/public/plugin.test.tsx | 7 +- x-pack/plugins/security/public/plugin.tsx | 4 +- 17 files changed, 424 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/cloud/public/mocks.ts create mode 100644 x-pack/plugins/cloud/public/user_menu_links.ts create mode 100644 x-pack/plugins/security/public/nav_control/index.mock.ts create mode 100644 x-pack/plugins/security/public/nav_control/nav_control_component.scss diff --git a/x-pack/plugins/cloud/kibana.json b/x-pack/plugins/cloud/kibana.json index 27b35bcbdd88b..9bca2f30bd23c 100644 --- a/x-pack/plugins/cloud/kibana.json +++ b/x-pack/plugins/cloud/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "cloud"], - "optionalPlugins": ["usageCollection", "home"], + "optionalPlugins": ["usageCollection", "home", "security"], "server": true, "ui": true } diff --git a/x-pack/plugins/cloud/public/index.ts b/x-pack/plugins/cloud/public/index.ts index 39ef5f452c18b..680b2f1ad2bd6 100644 --- a/x-pack/plugins/cloud/public/index.ts +++ b/x-pack/plugins/cloud/public/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from '../../../../src/core/public'; import { CloudPlugin } from './plugin'; -export { CloudSetup } from './plugin'; +export { CloudSetup, CloudConfigType } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new CloudPlugin(initializerContext); } diff --git a/x-pack/plugins/cloud/public/mocks.ts b/x-pack/plugins/cloud/public/mocks.ts new file mode 100644 index 0000000000000..bafebbca4ecdd --- /dev/null +++ b/x-pack/plugins/cloud/public/mocks.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +function createSetupMock() { + return { + cloudId: 'mock-cloud-id', + isCloudEnabled: true, + resetPasswordUrl: 'reset-password-url', + accountUrl: 'account-url', + }; +} + +export const cloudMock = { + createSetup: createSetupMock, +}; diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 45005f3f5e422..bc410b89c30e7 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -6,40 +6,51 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { i18n } from '@kbn/i18n'; +import { SecurityPluginStart } from '../../security/public'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { createUserMenuLinks } from './user_menu_links'; -interface CloudConfigType { +export interface CloudConfigType { id?: string; resetPasswordUrl?: string; deploymentUrl?: string; + accountUrl?: string; } interface CloudSetupDependencies { home?: HomePublicPluginSetup; } +interface CloudStartDependencies { + security?: SecurityPluginStart; +} + export interface CloudSetup { cloudId?: string; cloudDeploymentUrl?: string; isCloudEnabled: boolean; + resetPasswordUrl?: string; + accountUrl?: string; } export class CloudPlugin implements Plugin { private config!: CloudConfigType; + private isCloudEnabled: boolean; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); + this.isCloudEnabled = false; } public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; - const isCloudEnabled = getIsCloudEnabled(id); + this.isCloudEnabled = getIsCloudEnabled(id); if (home) { - home.environment.update({ cloud: isCloudEnabled }); - if (isCloudEnabled) { + home.environment.update({ cloud: this.isCloudEnabled }); + if (this.isCloudEnabled) { home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); } } @@ -47,11 +58,11 @@ export class CloudPlugin implements Plugin { return { cloudId: id, cloudDeploymentUrl: deploymentUrl, - isCloudEnabled, + isCloudEnabled: this.isCloudEnabled, }; } - public start(coreStart: CoreStart) { + public start(coreStart: CoreStart, { security }: CloudStartDependencies) { const { deploymentUrl } = this.config; coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); if (deploymentUrl) { @@ -63,5 +74,10 @@ export class CloudPlugin implements Plugin { href: deploymentUrl, }); } + + if (security && this.isCloudEnabled) { + const userMenuLinks = createUserMenuLinks(this.config); + security.navControlService.addUserMenuLinks(userMenuLinks); + } } } diff --git a/x-pack/plugins/cloud/public/user_menu_links.ts b/x-pack/plugins/cloud/public/user_menu_links.ts new file mode 100644 index 0000000000000..15e2f14e885ba --- /dev/null +++ b/x-pack/plugins/cloud/public/user_menu_links.ts @@ -0,0 +1,38 @@ +/* + * 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 { UserMenuLink } from '../../security/public'; +import { CloudConfigType } from '.'; + +export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => { + const { resetPasswordUrl, accountUrl } = config; + const userMenuLinks = [] as UserMenuLink[]; + + if (resetPasswordUrl) { + userMenuLinks.push({ + label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', { + defaultMessage: 'Cloud profile', + }), + iconType: 'logoCloud', + href: resetPasswordUrl, + order: 100, + }); + } + + if (accountUrl) { + userMenuLinks.push({ + label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', { + defaultMessage: 'Account & Billing', + }), + iconType: 'gear', + href: accountUrl, + order: 200, + }); + } + + return userMenuLinks; +}; diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index ff8a2c5acdf9a..eaa4ab7a482dd 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -23,6 +23,7 @@ const configSchema = schema.object({ apm: schema.maybe(apmConfigSchema), resetPasswordUrl: schema.maybe(schema.string()), deploymentUrl: schema.maybe(schema.string()), + accountUrl: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -32,6 +33,7 @@ export const config: PluginConfigDescriptor = { id: true, resetPasswordUrl: true, deploymentUrl: true, + accountUrl: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index 8016c94224060..d0382c22ed3c6 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -16,6 +16,7 @@ import { export { SecurityPluginSetup, SecurityPluginStart }; export { AuthenticatedUser } from '../common/model'; export { SecurityLicense, SecurityLicenseFeatures } from '../common/licensing'; +export { UserMenuLink } from '../public/nav_control'; export const plugin: PluginInitializer< SecurityPluginSetup, diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts index 33c1d1446afba..26a759ca52267 100644 --- a/x-pack/plugins/security/public/mocks.ts +++ b/x-pack/plugins/security/public/mocks.ts @@ -7,6 +7,7 @@ import { authenticationMock } from './authentication/index.mock'; import { createSessionTimeoutMock } from './session/session_timeout.mock'; import { licenseMock } from '../common/licensing/index.mock'; +import { navControlServiceMock } from './nav_control/index.mock'; function createSetupMock() { return { @@ -15,7 +16,13 @@ function createSetupMock() { license: licenseMock.create(), }; } +function createStartMock() { + return { + navControlService: navControlServiceMock.createStart(), + }; +} export const securityMock = { createSetup: createSetupMock, + createStart: createStartMock, }; diff --git a/x-pack/plugins/security/public/nav_control/index.mock.ts b/x-pack/plugins/security/public/nav_control/index.mock.ts new file mode 100644 index 0000000000000..1cd10810d7c8f --- /dev/null +++ b/x-pack/plugins/security/public/nav_control/index.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecurityNavControlServiceStart } from '.'; + +export const navControlServiceMock = { + createStart: (): jest.Mocked => ({ + getUserMenuLinks$: jest.fn(), + addUserMenuLinks: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/nav_control/index.ts b/x-pack/plugins/security/public/nav_control/index.ts index 2b0af1a45d05a..737ae50054698 100644 --- a/x-pack/plugins/security/public/nav_control/index.ts +++ b/x-pack/plugins/security/public/nav_control/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SecurityNavControlService } from './nav_control_service'; +export { SecurityNavControlService, SecurityNavControlServiceStart } from './nav_control_service'; +export { UserMenuLink } from './nav_control_component'; diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.scss b/x-pack/plugins/security/public/nav_control/nav_control_component.scss new file mode 100644 index 0000000000000..a3e04b08cfac2 --- /dev/null +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.scss @@ -0,0 +1,11 @@ +.chrNavControl__userMenu { + .euiContextMenuPanelTitle { + // Uppercased by default, override to match actual username + text-transform: none; + } + + .euiContextMenuItem { + // Temp fix for EUI issue https://github.com/elastic/eui/issues/3092 + line-height: normal; + } +} \ No newline at end of file diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index c1c6a9f69b6ec..1da91e80d062d 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import { shallowWithIntl, nextTick, mountWithIntl } from 'test_utils/enzyme_helpers'; import { SecurityNavControl } from './nav_control_component'; import { AuthenticatedUser } from '../../common/model'; @@ -17,6 +18,7 @@ describe('SecurityNavControl', () => { user: new Promise(() => {}) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = shallowWithIntl(); @@ -42,6 +44,7 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = shallowWithIntl(); @@ -70,6 +73,7 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = mountWithIntl(); @@ -91,6 +95,7 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = mountWithIntl(); @@ -107,4 +112,37 @@ describe('SecurityNavControl', () => { expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1); expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); }); + + it('renders a popover with additional user menu links registered by other plugins', async () => { + const props = { + user: Promise.resolve({ full_name: 'foo' }) as Promise, + editProfileUrl: '', + logoutUrl: '', + userMenuLinks$: new BehaviorSubject([ + { label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 }, + { label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2 }, + { label: 'link3', href: 'path-to-link-3', iconType: 'empty', order: 3 }, + ]), + }; + + const wrapper = mountWithIntl(); + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(0); + expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(0); + expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(0); + expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(0); + expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(0); + expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(0); + + wrapper.find(EuiHeaderSectionItemButton).simulate('click'); + + expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(1); + expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1); + expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(1); + expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(1); + expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(1); + expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 3ddabb0dc55f8..c22308fa8a43e 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -7,38 +7,52 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; - +import { Observable, Subscription } from 'rxjs'; import { EuiAvatar, - EuiFlexGroup, - EuiFlexItem, EuiHeaderSectionItemButton, - EuiLink, - EuiText, - EuiSpacer, EuiPopover, EuiLoadingSpinner, + EuiIcon, + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + IconType, + EuiText, } from '@elastic/eui'; import { AuthenticatedUser } from '../../common/model'; +import './nav_control_component.scss'; + +export interface UserMenuLink { + label: string; + iconType: IconType; + href: string; + order?: number; +} + interface Props { user: Promise; editProfileUrl: string; logoutUrl: string; + userMenuLinks$: Observable; } interface State { isOpen: boolean; authenticatedUser: AuthenticatedUser | null; + userMenuLinks: UserMenuLink[]; } export class SecurityNavControl extends Component { + private subscription?: Subscription; + constructor(props: Props) { super(props); this.state = { isOpen: false, authenticatedUser: null, + userMenuLinks: [], }; props.user.then((authenticatedUser) => { @@ -48,6 +62,18 @@ export class SecurityNavControl extends Component { }); } + componentDidMount() { + this.subscription = this.props.userMenuLinks$.subscribe(async (userMenuLinks) => { + this.setState({ userMenuLinks }); + }); + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + onMenuButtonClick = () => { if (!this.state.authenticatedUser) { return; @@ -66,13 +92,13 @@ export class SecurityNavControl extends Component { render() { const { editProfileUrl, logoutUrl } = this.props; - const { authenticatedUser } = this.state; + const { authenticatedUser, userMenuLinks } = this.state; - const name = + const username = (authenticatedUser && (authenticatedUser.full_name || authenticatedUser.username)) || ''; const buttonContents = authenticatedUser ? ( - + ) : ( ); @@ -92,6 +118,60 @@ export class SecurityNavControl extends Component { ); + const profileMenuItem = { + name: ( + + ), + icon: , + href: editProfileUrl, + 'data-test-subj': 'profileLink', + }; + + const logoutMenuItem = { + name: ( + + ), + icon: , + href: logoutUrl, + 'data-test-subj': 'logoutLink', + }; + + const items: EuiContextMenuPanelItemDescriptor[] = []; + + items.push(profileMenuItem); + + if (userMenuLinks.length) { + const userMenuLinkMenuItems = userMenuLinks + .sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB) + .map(({ label, iconType, href }: UserMenuLink) => ({ + name: {label}, + icon: , + href, + 'data-test-subj': `userMenuLink__${label}`, + })); + + items.push(...userMenuLinkMenuItems, { + isSeparator: true, + key: 'securityNavControlComponent__userMenuLinksSeparator', + }); + } + + items.push(logoutMenuItem); + + const panels = [ + { + id: 0, + title: username, + items, + }, + ]; + return ( { repositionOnScroll closePopover={this.closeMenu} panelPaddingSize="none" + buffer={0} > -
- - - - - - - -

{name}

-
- - - - - - - - - - - - - - - - - - - - -
-
+
+
); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts index acf62f3376b8b..5b9788d67500b 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts @@ -173,4 +173,134 @@ describe('SecurityNavControlService', () => { navControlService.start({ core: coreStart }); expect(coreStart.chrome.navControls.registerRight).toHaveBeenCalledTimes(2); }); + + describe(`#start`, () => { + it('should return functions to register and retrieve user menu links', () => { + const license$ = new BehaviorSubject(validLicense); + + const navControlService = new SecurityNavControlService(); + navControlService.setup({ + securityLicense: new SecurityLicenseService().setup({ license$ }).license, + authc: securityMock.createSetup().authc, + logoutUrl: '/some/logout/url', + }); + + const coreStart = coreMock.createStart(); + const navControlServiceStart = navControlService.start({ core: coreStart }); + expect(navControlServiceStart).toHaveProperty('getUserMenuLinks$'); + expect(navControlServiceStart).toHaveProperty('addUserMenuLinks'); + }); + + it('should register custom user menu links to be displayed in the nav controls', (done) => { + const license$ = new BehaviorSubject(validLicense); + + const navControlService = new SecurityNavControlService(); + navControlService.setup({ + securityLicense: new SecurityLicenseService().setup({ license$ }).license, + authc: securityMock.createSetup().authc, + logoutUrl: '/some/logout/url', + }); + + const coreStart = coreMock.createStart(); + const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart }); + const userMenuLinks$ = getUserMenuLinks$(); + + addUserMenuLinks([ + { + label: 'link1', + href: 'path-to-link1', + iconType: 'empty', + }, + ]); + + userMenuLinks$.subscribe((links) => { + expect(links).toMatchInlineSnapshot(` + Array [ + Object { + "href": "path-to-link1", + "iconType": "empty", + "label": "link1", + }, + ] + `); + done(); + }); + }); + + it('should retrieve user menu links sorted by order', (done) => { + const license$ = new BehaviorSubject(validLicense); + + const navControlService = new SecurityNavControlService(); + navControlService.setup({ + securityLicense: new SecurityLicenseService().setup({ license$ }).license, + authc: securityMock.createSetup().authc, + logoutUrl: '/some/logout/url', + }); + + const coreStart = coreMock.createStart(); + const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart }); + const userMenuLinks$ = getUserMenuLinks$(); + + addUserMenuLinks([ + { + label: 'link3', + href: 'path-to-link3', + iconType: 'empty', + order: 3, + }, + { + label: 'link1', + href: 'path-to-link1', + iconType: 'empty', + order: 1, + }, + { + label: 'link2', + href: 'path-to-link2', + iconType: 'empty', + order: 2, + }, + ]); + addUserMenuLinks([ + { + label: 'link4', + href: 'path-to-link4', + iconType: 'empty', + order: 4, + }, + ]); + + userMenuLinks$.subscribe((links) => { + expect(links).toMatchInlineSnapshot(` + Array [ + Object { + "href": "path-to-link1", + "iconType": "empty", + "label": "link1", + "order": 1, + }, + Object { + "href": "path-to-link2", + "iconType": "empty", + "label": "link2", + "order": 2, + }, + Object { + "href": "path-to-link3", + "iconType": "empty", + "label": "link3", + "order": 3, + }, + Object { + "href": "path-to-link4", + "iconType": "empty", + "label": "link4", + "order": 4, + }, + ] + `); + done(); + }); + }); + }); }); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx index aa3ec2e47469d..5d2e7d7dfb733 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Subscription } from 'rxjs'; +import { sortBy } from 'lodash'; +import { Observable, Subscription, BehaviorSubject, ReplaySubject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; + import ReactDOM from 'react-dom'; import React from 'react'; + import { SecurityLicense } from '../../common/licensing'; -import { SecurityNavControl } from './nav_control_component'; +import { SecurityNavControl, UserMenuLink } from './nav_control_component'; import { AuthenticationServiceSetup } from '../authentication'; interface SetupDeps { @@ -22,6 +26,18 @@ interface StartDeps { core: CoreStart; } +export interface SecurityNavControlServiceStart { + /** + * Returns an Observable of the array of user menu links registered by other plugins + */ + getUserMenuLinks$: () => Observable; + + /** + * Registers the provided user menu links to be displayed in the user menu in the global nav + */ + addUserMenuLinks: (newUserMenuLink: UserMenuLink[]) => void; +} + export class SecurityNavControlService { private securityLicense!: SecurityLicense; private authc!: AuthenticationServiceSetup; @@ -31,13 +47,16 @@ export class SecurityNavControlService { private securityFeaturesSubscription?: Subscription; + private readonly stop$ = new ReplaySubject(1); + private userMenuLinks$ = new BehaviorSubject([]); + public setup({ securityLicense, authc, logoutUrl }: SetupDeps) { this.securityLicense = securityLicense; this.authc = authc; this.logoutUrl = logoutUrl; } - public start({ core }: StartDeps) { + public start({ core }: StartDeps): SecurityNavControlServiceStart { this.securityFeaturesSubscription = this.securityLicense.features$.subscribe( ({ showLinks }) => { const isAnonymousPath = core.http.anonymousPaths.isAnonymous(window.location.pathname); @@ -49,6 +68,16 @@ export class SecurityNavControlService { } } ); + + return { + getUserMenuLinks$: () => + this.userMenuLinks$.pipe(map(this.sortUserMenuLinks), takeUntil(this.stop$)), + addUserMenuLinks: (userMenuLinks: UserMenuLink[]) => { + const currentLinks = this.userMenuLinks$.value; + const newLinks = [...currentLinks, ...userMenuLinks]; + this.userMenuLinks$.next(newLinks); + }, + }; } public stop() { @@ -57,6 +86,7 @@ export class SecurityNavControlService { this.securityFeaturesSubscription = undefined; } this.navControlRegistered = false; + this.stop$.next(); } private registerSecurityNavControl( @@ -72,6 +102,7 @@ export class SecurityNavControlService { user: currentUserPromise, editProfileUrl: core.http.basePath.prepend('/security/account'), logoutUrl: this.logoutUrl, + userMenuLinks$: this.userMenuLinks$, }; ReactDOM.render( @@ -86,4 +117,8 @@ export class SecurityNavControlService { this.navControlRegistered = true; } + + private sortUserMenuLinks(userMenuLinks: UserMenuLink[]) { + return sortBy(userMenuLinks, 'order'); + } } diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index d86d4812af5e3..6f5a2a031a7b2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -97,7 +97,12 @@ describe('Security Plugin', () => { data: {} as DataPublicPluginStart, features: {} as FeaturesPluginStart, }) - ).toBeUndefined(); + ).toEqual({ + navControlService: { + getUserMenuLinks$: expect.any(Function), + addUserMenuLinks: expect.any(Function), + }, + }); }); it('starts Management Service if `management` plugin is available', () => { diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 700653c4cecb8..f94772c43dd89 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -146,11 +146,13 @@ export class SecurityPlugin public start(core: CoreStart, { management, securityOss }: PluginStartDependencies) { this.sessionTimeout.start(); - this.navControlService.start({ core }); this.securityCheckupService.start({ securityOssStart: securityOss, docLinks: core.docLinks }); + if (management) { this.managementService.start({ capabilities: core.application.capabilities }); } + + return { navControlService: this.navControlService.start({ core }) }; } public stop() { From 8ff92f2b4051faa4969a909c858ca6ae0fdace52 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 10 Nov 2020 10:30:45 -0600 Subject: [PATCH 18/19] [Workplace Search] Consolidate groups routes (#83015) * [Workplace Search] Consolidate groups routes This PR consolidates all of the groups route resgistration functions into a single export so that the `registerWorkplaceSearchRoutes` function only has to call the top-level routes * Remove redundant test --- .../server/routes/workplace_search/groups.ts | 10 ++++++++++ .../server/routes/workplace_search/index.ts | 18 ++---------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts index 35c585eb9f781..fa01f983bbb89 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts @@ -223,3 +223,13 @@ export function registerBoostsGroupRoute({ } ); } + +export const registerGroupsRoutes = (dependencies: IRouteDependencies) => { + registerGroupsRoute(dependencies); + registerSearchGroupsRoute(dependencies); + registerGroupRoute(dependencies); + registerGroupUsersRoute(dependencies); + registerShareGroupRoute(dependencies); + registerAssignGroupRoute(dependencies); + registerBoostsGroupRoute(dependencies); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts index a5ebcc0d05298..0edf0b980cf21 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -7,23 +7,9 @@ import { IRouteDependencies } from '../../plugin'; import { registerOverviewRoute } from './overview'; -import { - registerGroupsRoute, - registerSearchGroupsRoute, - registerGroupRoute, - registerGroupUsersRoute, - registerShareGroupRoute, - registerAssignGroupRoute, - registerBoostsGroupRoute, -} from './groups'; +import { registerGroupsRoutes } from './groups'; export const registerWorkplaceSearchRoutes = (dependencies: IRouteDependencies) => { registerOverviewRoute(dependencies); - registerGroupsRoute(dependencies); - registerSearchGroupsRoute(dependencies); - registerGroupRoute(dependencies); - registerGroupUsersRoute(dependencies); - registerShareGroupRoute(dependencies); - registerAssignGroupRoute(dependencies); - registerBoostsGroupRoute(dependencies); + registerGroupsRoutes(dependencies); }; From 915f718c6ec226421c7ada18f667d7fa721d4271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Tue, 10 Nov 2020 17:36:35 +0100 Subject: [PATCH 19/19] [Security Solution] Fix DNS Network table query (#82778) --- .../components/paginated_table/helpers.ts | 5 +-- .../network/containers/network_dns/index.tsx | 4 +-- .../factory/network/dns/__mocks__/index.ts | 34 +++++++++++++++--- .../factory/network/dns/index.ts | 5 ++- .../network/dns/query.dns_network.dsl.ts | 36 +++++++++++-------- .../apis/security_solution/network_dns.ts | 5 ++- 6 files changed, 60 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/helpers.ts b/x-pack/plugins/security_solution/public/common/components/paginated_table/helpers.ts index 8fde81adc922a..9685a260d2a1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/helpers.ts @@ -8,13 +8,14 @@ import { PaginationInputPaginated } from '../../../graphql/types'; export const generateTablePaginationOptions = ( activePage: number, - limit: number + limit: number, + isBucketSort?: boolean ): PaginationInputPaginated => { const cursorStart = activePage * limit; return { activePage, cursorStart, fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, - querySize: limit + cursorStart, + querySize: isBucketSort ? limit : limit + cursorStart, }; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index fc00f8866ed2e..108bfa0c9df69 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -77,7 +77,7 @@ export const useNetworkDns = ({ factoryQueryType: NetworkQueries.dns, filterQuery: createFilter(filterQuery), isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit), + pagination: generateTablePaginationOptions(activePage, limit, true), sort, timerange: { interval: '12h', @@ -193,7 +193,7 @@ export const useNetworkDns = ({ isPtrIncluded, factoryQueryType: NetworkQueries.dns, filterQuery: createFilter(filterQuery), - pagination: generateTablePaginationOptions(activePage, limit), + pagination: generateTablePaginationOptions(activePage, limit, true), sort, timerange: { interval: '12h', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/__mocks__/index.ts index d3625a96c6db9..d4cef30f3b320 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/__mocks__/index.ts @@ -146,10 +146,23 @@ export const formattedSearchStrategyResponse = { dns_name_query_count: { terms: { field: 'dns.question.registered_domain', - size: 10, - order: { unique_domains: 'desc' }, + size: 1000000, }, aggs: { + bucket_sort: { + bucket_sort: { + sort: [ + { + unique_domains: { + order: 'desc', + }, + }, + { _key: { order: 'asc' } }, + ], + from: 0, + size: 10, + }, + }, unique_domains: { cardinality: { field: 'dns.question.name' } }, dns_bytes_in: { sum: { field: 'source.bytes' } }, dns_bytes_out: { sum: { field: 'destination.bytes' } }, @@ -204,10 +217,23 @@ export const expectedDsl = { dns_name_query_count: { terms: { field: 'dns.question.registered_domain', - size: 10, - order: { unique_domains: 'desc' }, + size: 1000000, }, aggs: { + bucket_sort: { + bucket_sort: { + sort: [ + { + unique_domains: { + order: 'desc', + }, + }, + { _key: { order: 'asc' } }, + ], + from: 0, + size: 10, + }, + }, unique_domains: { cardinality: { field: 'dns.question.name' } }, dns_bytes_in: { sum: { field: 'source.bytes' } }, dns_bytes_out: { sum: { field: 'destination.bytes' } }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts index ca7743126df4c..758731b674544 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts @@ -33,11 +33,10 @@ export const networkDns: SecuritySolutionFactory = { options: NetworkDnsRequestOptions, response: IEsSearchResponse ): Promise => { - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const { activePage, fakePossibleCount } = options.pagination; const totalCount = getOr(0, 'aggregations.dns_count.value', response.rawResponse); - const networkDnsEdges: NetworkDnsEdges[] = getDnsEdges(response); + const edges: NetworkDnsEdges[] = getDnsEdges(response); const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = networkDnsEdges.splice(cursorStart, querySize - cursorStart); const inspect = { dsl: [inspectStringifyObject(buildDnsQuery(options))], }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts index 85b9051189bfe..7043b15ebb4dd 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts @@ -15,25 +15,27 @@ import { } from '../../../../../../common/search_strategy'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; +const HUGE_QUERY_SIZE = 1000000; + type QueryOrder = - | { _count: Direction } - | { _key: Direction } - | { unique_domains: Direction } - | { dns_bytes_in: Direction } - | { dns_bytes_out: Direction }; + | { _count: { order: Direction } } + | { _key: { order: Direction } } + | { unique_domains: { order: Direction } } + | { dns_bytes_in: { order: Direction } } + | { dns_bytes_out: { order: Direction } }; const getQueryOrder = (sort: SortField): QueryOrder => { switch (sort.field) { case NetworkDnsFields.queryCount: - return { _count: sort.direction }; + return { _count: { order: sort.direction } }; case NetworkDnsFields.dnsName: - return { _key: sort.direction }; + return { _key: { order: sort.direction } }; case NetworkDnsFields.uniqueDomains: - return { unique_domains: sort.direction }; + return { unique_domains: { order: sort.direction } }; case NetworkDnsFields.dnsBytesIn: - return { dns_bytes_in: sort.direction }; + return { dns_bytes_in: { order: sort.direction } }; case NetworkDnsFields.dnsBytesOut: - return { dns_bytes_out: sort.direction }; + return { dns_bytes_out: { order: sort.direction } }; } assertUnreachable(sort.field); }; @@ -67,7 +69,7 @@ export const buildDnsQuery = ({ filterQuery, isPtrIncluded, sort, - pagination: { querySize }, + pagination: { cursorStart, querySize }, stackByField = 'dns.question.registered_domain', timerange: { from, to }, }: NetworkDnsRequestOptions) => { @@ -95,12 +97,16 @@ export const buildDnsQuery = ({ dns_name_query_count: { terms: { field: stackByField, - size: querySize, - order: { - ...getQueryOrder(sort), - }, + size: HUGE_QUERY_SIZE, }, aggs: { + bucket_sort: { + bucket_sort: { + sort: [getQueryOrder(sort), { _key: { order: 'asc' } }], + from: cursorStart, + size: querySize, + }, + }, unique_domains: { cardinality: { field: 'dns.question.name', diff --git a/x-pack/test/api_integration/apis/security_solution/network_dns.ts b/x-pack/test/api_integration/apis/security_solution/network_dns.ts index 966b8184965d1..9b7a39a279773 100644 --- a/x-pack/test/api_integration/apis/security_solution/network_dns.ts +++ b/x-pack/test/api_integration/apis/security_solution/network_dns.ts @@ -18,8 +18,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - // Failing: See https://github.com/elastic/kibana/issues/82207 - describe.skip('Network DNS', () => { + describe('Network DNS', () => { describe('With packetbeat', () => { before(() => esArchiver.load('packetbeat/dns')); after(() => esArchiver.unload('packetbeat/dns')); @@ -59,7 +58,7 @@ export default function ({ getService }: FtrProviderContext) { expect(networkDns.edges.length).to.be(10); expect(networkDns.totalCount).to.be(44); expect(networkDns.edges.map((i: NetworkDnsEdges) => i.node.dnsName).join(',')).to.be( - 'aaplimg.com,adgrx.com,akadns.net,akamaiedge.net,amazonaws.com,cbsistatic.com,cdn-apple.com,connman.net,crowbird.com,d1oxlq5h9kq8q5.cloudfront.net' + 'aaplimg.com,adgrx.com,akadns.net,akamaiedge.net,amazonaws.com,cbsistatic.com,cdn-apple.com,connman.net,d1oxlq5h9kq8q5.cloudfront.net,d3epxf4t8a32oh.cloudfront.net' ); expect(networkDns.pageInfo.fakeTotalCount).to.equal(30); });