From 659890cc91072b5c1477580355cdd39c1526a0ab Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Fri, 21 Aug 2020 09:25:17 +0100 Subject: [PATCH 01/77] [ML] Transforms: Unset doc title when app unmounts (#75539) Co-authored-by: Elastic Machine --- .../transform/public/app/mount_management_section.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 454738f7a313a5..0392ecbafa832f 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -49,5 +49,10 @@ export async function mountManagementSection( history, }; - return renderApp(element, appDependencies); + const unmountAppCallback = renderApp(element, appDependencies); + + return () => { + docTitle.reset(); + unmountAppCallback(); + }; } From 3ddc2acd66208599c260397ed34ee12b6f1b67fa Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Fri, 21 Aug 2020 11:16:52 +0200 Subject: [PATCH 02/77] adding markdown vis renderer (#75532) --- src/plugins/vis_type_markdown/kibana.json | 2 +- .../__snapshots__/markdown_fn.test.ts.snap | 2 +- .../public/__snapshots__/to_ast.test.ts.snap | 67 +++++++++++++++++ .../vis_type_markdown/public/markdown_fn.ts | 8 +- .../public/markdown_renderer.tsx | 57 ++++++++++++++ .../vis_type_markdown/public/markdown_vis.ts | 4 +- .../public/markdown_vis_controller.test.tsx | 74 ++++++++++++------- .../public/markdown_vis_controller.tsx | 11 ++- .../vis_type_markdown/public/plugin.ts | 6 +- .../vis_type_markdown/public/to_ast.test.ts | 54 ++++++++++++++ .../vis_type_markdown/public/to_ast.ts | 39 ++++++++++ .../__snapshots__/build_pipeline.test.ts.snap | 4 - .../public/legacy/build_pipeline.test.ts | 17 ----- .../public/legacy/build_pipeline.ts | 11 --- 14 files changed, 288 insertions(+), 68 deletions(-) create mode 100644 src/plugins/vis_type_markdown/public/__snapshots__/to_ast.test.ts.snap create mode 100644 src/plugins/vis_type_markdown/public/markdown_renderer.tsx create mode 100644 src/plugins/vis_type_markdown/public/to_ast.test.ts create mode 100644 src/plugins/vis_type_markdown/public/to_ast.ts diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json index 9241f5eeee8371..4196bd7e857076 100644 --- a/src/plugins/vis_type_markdown/kibana.json +++ b/src/plugins/vis_type_markdown/kibana.json @@ -4,5 +4,5 @@ "ui": true, "server": true, "requiredPlugins": ["expressions", "visualizations"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "charts"] + "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "charts", "visualizations", "expressions"] } diff --git a/src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap b/src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap index 5a107bdfed9e52..473e2cba742b7a 100644 --- a/src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap +++ b/src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap @@ -2,7 +2,7 @@ exports[`interpreter/functions#markdown returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "markdown_vis", "type": "render", "value": Object { "visConfig": Object { diff --git a/src/plugins/vis_type_markdown/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_markdown/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 00000000000000..2b8ff47be3f23d --- /dev/null +++ b/src/plugins/vis_type_markdown/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`markdown vis toExpressionAst function with params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "font": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "size": Array [ + 15, + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "markdown": Array [ + "### my markdown", + ], + "openLinksInNewTab": Array [ + true, + ], + }, + "function": "markdownVis", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`markdown vis toExpressionAst function without params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "font": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "size": Array [ + "undefined", + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "markdownVis", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/vis_type_markdown/public/markdown_fn.ts b/src/plugins/vis_type_markdown/public/markdown_fn.ts index 9f0809109e4653..4b3c9989431f9b 100644 --- a/src/plugins/vis_type_markdown/public/markdown_fn.ts +++ b/src/plugins/vis_type_markdown/public/markdown_fn.ts @@ -26,12 +26,14 @@ interface RenderValue { visConfig: MarkdownVisParams; } -export const createMarkdownVisFn = (): ExpressionFunctionDefinition< +export type MarkdownVisExpressionFunctionDefinition = ExpressionFunctionDefinition< 'markdownVis', unknown, Arguments, Render -> => ({ +>; + +export const createMarkdownVisFn = (): MarkdownVisExpressionFunctionDefinition => ({ name: 'markdownVis', type: 'render', inputTypes: [], @@ -65,7 +67,7 @@ export const createMarkdownVisFn = (): ExpressionFunctionDefinition< fn(input, args) { return { type: 'render', - as: 'visualization', + as: 'markdown_vis', value: { visType: 'markdown', visConfig: { diff --git a/src/plugins/vis_type_markdown/public/markdown_renderer.tsx b/src/plugins/vis_type_markdown/public/markdown_renderer.tsx new file mode 100644 index 00000000000000..5950a762635b28 --- /dev/null +++ b/src/plugins/vis_type_markdown/public/markdown_renderer.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { VisualizationContainer } from '../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; +import { MarkdownVisWrapper } from './markdown_vis_controller'; +import { StartServicesGetter } from '../../kibana_utils/public'; + +export const getMarkdownRenderer = (start: StartServicesGetter) => { + const markdownVisRenderer: () => ExpressionRenderDefinition = () => ({ + name: 'markdown_vis', + displayName: 'markdown visualization', + reuseDomNode: true, + render: async (domNode: HTMLElement, config: any, handlers: any) => { + const { visConfig } = config; + + const I18nContext = await start().core.i18n.Context; + + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + + + + + , + domNode + ); + }, + }); + + return markdownVisRenderer; +}; diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index 089e00bb449373..27ac038aee6fff 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -19,10 +19,10 @@ import { i18n } from '@kbn/i18n'; -import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; import { SettingsOptions } from './settings_options_lazy'; import { DefaultEditorSize } from '../../vis_default_editor/public'; +import { toExpressionAst } from './to_ast'; export const markdownVisDefinition = { name: 'markdown', @@ -32,8 +32,8 @@ export const markdownVisDefinition = { description: i18n.translate('visTypeMarkdown.markdownDescription', { defaultMessage: 'Create a document using markdown syntax', }), + toExpressionAst, visConfig: { - component: MarkdownVisWrapper, defaults: { fontSize: 12, openLinksInNewTab: false, diff --git a/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx index 103879cb6e6dfb..ff0cc89a5d9c90 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx @@ -25,13 +25,15 @@ describe('markdown vis controller', () => { it('should set html from markdown params', () => { const vis = { params: { + openLinksInNewTab: false, + fontSize: 16, markdown: 'This is a test of the [markdown](http://daringfireball.net/projects/markdown) vis.', }, }; const wrapper = render( - + ); expect(wrapper.find('a').text()).toBe('markdown'); }); @@ -39,12 +41,14 @@ describe('markdown vis controller', () => { it('should not render the html', () => { const vis = { params: { + openLinksInNewTab: false, + fontSize: 16, markdown: 'Testing html', }, }; const wrapper = render( - + ); expect(wrapper.text()).toBe('Testing html\n'); }); @@ -52,12 +56,14 @@ describe('markdown vis controller', () => { it('should update the HTML when render again with changed params', () => { const vis = { params: { + openLinksInNewTab: false, + fontSize: 16, markdown: 'Initial', }, }; const wrapper = mount( - + ); expect(wrapper.text().trim()).toBe('Initial'); vis.params.markdown = 'Updated'; @@ -66,52 +72,68 @@ describe('markdown vis controller', () => { }); describe('renderComplete', () => { + const vis = { + params: { + openLinksInNewTab: false, + fontSize: 16, + markdown: 'test', + }, + }; + + const renderComplete = jest.fn(); + + beforeEach(() => { + renderComplete.mockClear(); + }); + it('should be called on initial rendering', () => { - const vis = { - params: { - markdown: 'test', - }, - }; - const renderComplete = jest.fn(); mount( - + ); expect(renderComplete.mock.calls.length).toBe(1); }); it('should be called on successive render when params change', () => { - const vis = { - params: { - markdown: 'test', - }, - }; - const renderComplete = jest.fn(); mount( - + ); expect(renderComplete.mock.calls.length).toBe(1); renderComplete.mockClear(); vis.params.markdown = 'changed'; mount( - + ); expect(renderComplete.mock.calls.length).toBe(1); }); it('should be called on successive render even without data change', () => { - const vis = { - params: { - markdown: 'test', - }, - }; - const renderComplete = jest.fn(); mount( - + ); expect(renderComplete.mock.calls.length).toBe(1); renderComplete.mockClear(); mount( - + ); expect(renderComplete.mock.calls.length).toBe(1); }); diff --git a/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx index 4e77bb196b713d..e1155ca42df723 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx @@ -22,7 +22,7 @@ import { Markdown } from '../../kibana_react/public'; import { MarkdownVisParams } from './types'; interface MarkdownVisComponentProps extends MarkdownVisParams { - renderComplete: () => {}; + renderComplete: () => void; } /** @@ -80,7 +80,14 @@ class MarkdownVisComponent extends React.Component { * The way React works, this wrapper nearly brings no overhead, but allows us * to use proper lifecycle methods in the actual component. */ -export function MarkdownVisWrapper(props: any) { + +export interface MarkdownVisWrapperProps { + visParams: MarkdownVisParams; + fireEvent: (event: any) => void; + renderComplete: () => void; +} + +export function MarkdownVisWrapper(props: MarkdownVisWrapperProps) { return ( { } public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) { - visualizations.createReactVisualization(markdownVisDefinition); + const start = createStartServicesGetter(core.getStartServices); + visualizations.createBaseVisualization(markdownVisDefinition); + expressions.registerRenderer(getMarkdownRenderer(start)); expressions.registerFunction(createMarkdownVisFn); } diff --git a/src/plugins/vis_type_markdown/public/to_ast.test.ts b/src/plugins/vis_type_markdown/public/to_ast.test.ts new file mode 100644 index 00000000000000..1ad1fa0ee25176 --- /dev/null +++ b/src/plugins/vis_type_markdown/public/to_ast.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { toExpressionAst } from './to_ast'; +import { Vis } from '../../visualizations/public'; + +describe('markdown vis toExpressionAst function', () => { + let vis: Vis; + + beforeEach(() => { + vis = { + isHierarchical: () => false, + type: {}, + params: { + percentageMode: false, + }, + data: { + indexPattern: { id: '123' } as any, + aggs: { + getResponseAggs: () => [], + aggs: [], + } as any, + }, + } as any; + }); + + it('without params', () => { + vis.params = {}; + const actual = toExpressionAst(vis); + expect(actual).toMatchSnapshot(); + }); + + it('with params', () => { + vis.params = { markdown: '### my markdown', fontSize: 15, openLinksInNewTab: true }; + const actual = toExpressionAst(vis); + expect(actual).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_markdown/public/to_ast.ts b/src/plugins/vis_type_markdown/public/to_ast.ts new file mode 100644 index 00000000000000..9b481218b42ea7 --- /dev/null +++ b/src/plugins/vis_type_markdown/public/to_ast.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { MarkdownVisExpressionFunctionDefinition } from './markdown_fn'; + +export const toExpressionAst = (vis: Vis) => { + const { markdown, fontSize, openLinksInNewTab } = vis.params; + + const markdownVis = buildExpressionFunction( + 'markdownVis', + { + markdown, + font: buildExpression(`font size=${fontSize}`), + openLinksInNewTab, + } + ); + + const ast = buildExpression([markdownVis]); + + return ast.toAst(); +}; diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index df29c078d23e41..fae777b98ef634 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -4,8 +4,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls t exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"markdownvis '## hello _markdown_' font={font size=12} openLinksInNewTab=true "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles pie function 1`] = `"kibana_pie visConfig='{\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"buckets\\":[1,2]}}' "`; @@ -34,6 +32,4 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles timelion function 1`] = `"timelion_vis expression='foo' interval='bar' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles undefined markdown function 1`] = `"markdownvis '' font={font size=12} openLinksInNewTab=true "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 50fa5ac64e2a12..2d92b386253b03 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -123,23 +123,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); - it('handles markdown function', () => { - const params = { - markdown: '## hello _markdown_', - fontSize: 12, - openLinksInNewTab: true, - foo: 'bar', - }; - const actual = buildPipelineVisFunction.markdown(params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('handles undefined markdown function', () => { - const params = { fontSize: 12, openLinksInNewTab: true, foo: 'bar' }; - const actual = buildPipelineVisFunction.markdown(params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - describe('handles table function', () => { it('without splits or buckets', () => { const params = { foo: 'bar' }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index ebd240c79287a3..438a6d2337724e 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -269,17 +269,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { const interval = prepareString('interval', params.interval); return `timelion_vis ${expression}${interval}`; }, - markdown: (params) => { - const { markdown, fontSize, openLinksInNewTab } = params; - let escapedMarkdown = ''; - if (typeof markdown === 'string' || markdown instanceof String) { - escapedMarkdown = escapeString(markdown.toString()); - } - let expr = `markdownvis '${escapedMarkdown}' `; - expr += prepareValue('font', `{font size=${fontSize}}`, true); - expr += prepareValue('openLinksInNewTab', openLinksInNewTab); - return expr; - }, table: (params, schemas) => { const visConfig = { ...params, From 74ab9897b57daf746c0a41e739485032ddf6a15f Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 21 Aug 2020 11:28:37 +0200 Subject: [PATCH 03/77] Embeddable input (#73033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 move timeRange, filters and query to base embeddabl * refactor: 💡 use new base embeddable input in explore data * feat: 🎸 import types as types --- .../public/lib/embeddables/i_embeddable.ts | 16 ++++++++++++++++ .../public/embeddable/visualize_embeddable.ts | 3 --- .../explore_data/abstract_explore_data_action.ts | 1 + .../explore_data/explore_data_chart_action.ts | 9 +++------ .../explore_data_context_menu_action.ts | 7 +++---- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 7628b1d41b4528..9c4a1b5602c490 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -22,6 +22,7 @@ import { Adapters } from '../types'; import { IContainer } from '../containers/i_container'; import { ViewMode } from '../types'; import { TriggerContextMapping } from '../../../../ui_actions/public'; +import type { TimeRange, Query, Filter } from '../../../../data/common'; export interface EmbeddableError { name: string; @@ -55,6 +56,21 @@ export interface EmbeddableInput { */ disableTriggers?: boolean; + /** + * Time range of the chart. + */ + timeRange?: TimeRange; + + /** + * Visualization query string used to narrow down results. + */ + query?: Query; + + /** + * Visualization filters used to narrow down results. + */ + filters?: Filter[]; + [key: string]: unknown; } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index a434bf9756b64f..4efdfd2911cbcb 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -60,9 +60,6 @@ export interface VisualizeEmbeddableConfiguration { } export interface VisualizeInput extends EmbeddableInput { - timeRange?: TimeRange; - query?: Query; - filters?: Filter[]; vis?: { colors?: { [key: string]: string }; }; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 4ddcb3386f314c..36a844752a1c34 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -59,6 +59,7 @@ export abstract class AbstractExploreDataAction Date: Fri, 21 Aug 2020 11:33:42 +0200 Subject: [PATCH 04/77] [Lens] Use index pattern service instead saved object client (#74654) --- .../datapanel.test.tsx | 45 +- .../indexpattern_datasource/datapanel.tsx | 5 +- .../bucket_nesting_editor.test.tsx | 16 + .../dimension_panel/bucket_nesting_editor.tsx | 8 +- .../dimension_panel/dimension_panel.test.tsx | 35 +- .../dimension_panel/field_select.tsx | 6 +- .../dimension_panel/popover_editor.tsx | 1 + .../indexpattern_datasource/document_field.ts | 6 +- .../field_item.test.tsx | 15 + .../indexpattern_datasource/field_item.tsx | 6 +- .../indexpattern.test.ts | 9 + .../indexpattern_datasource/indexpattern.tsx | 7 +- .../indexpattern_suggestions.test.tsx | 41 +- .../indexpattern_suggestions.ts | 16 +- .../layerpanel.test.tsx | 11 + .../indexpattern_datasource/loader.test.ts | 554 +++++++++--------- .../public/indexpattern_datasource/loader.ts | 153 +++-- .../public/indexpattern_datasource/mocks.ts | 20 + .../operations/definitions/cardinality.tsx | 4 +- .../operations/definitions/count.tsx | 2 +- .../definitions/date_histogram.test.tsx | 7 + .../operations/definitions/date_histogram.tsx | 4 +- .../operations/definitions/metrics.tsx | 4 +- .../operations/definitions/terms.test.tsx | 8 + .../operations/definitions/terms.tsx | 4 +- .../operations/operations.test.ts | 10 + .../state_helpers.test.ts | 5 + .../public/indexpattern_datasource/types.ts | 1 + 28 files changed, 586 insertions(+), 417 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 4a79f30a17a059..8291b673cd17a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -87,36 +87,42 @@ const initialState: IndexPatternPrivateState = { 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, @@ -131,6 +137,7 @@ const initialState: IndexPatternPrivateState = { fields: [ { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -145,6 +152,7 @@ const initialState: IndexPatternPrivateState = { }, { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -166,6 +174,7 @@ const initialState: IndexPatternPrivateState = { }, { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -185,18 +194,21 @@ const initialState: IndexPatternPrivateState = { 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, @@ -581,18 +593,19 @@ describe('IndexPattern Data Panel', () => { .find('[data-test-subj="lnsIndexPatternAvailableFields"]') .find(FieldItem) .map((fieldItem) => fieldItem.prop('field').name) - ).toEqual(['bytes', 'memory']); + ).toEqual(['memory', 'bytes']); wrapper .find('[data-test-subj="lnsIndexPatternEmptyFields"]') .find('button') .first() .simulate('click'); + const emptyAccordion = wrapper.find('[data-test-subj="lnsIndexPatternEmptyFields"]'); expect( - wrapper - .find('[data-test-subj="lnsIndexPatternEmptyFields"]') - .find(FieldItem) - .map((fieldItem) => fieldItem.prop('field').name) + emptyAccordion.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name) ).toEqual(['client', 'source', 'timestamp']); + expect( + emptyAccordion.find(FieldItem).map((fieldItem) => fieldItem.prop('field').displayName) + ).toEqual(['client', 'source', 'timestampLabel']); }); it('should display NoFieldsCallout when all fields are empty', async () => { @@ -615,8 +628,8 @@ describe('IndexPattern Data Panel', () => { wrapper .find('[data-test-subj="lnsIndexPatternEmptyFields"]') .find(FieldItem) - .map((fieldItem) => fieldItem.prop('field').name) - ).toEqual(['bytes', 'client', 'memory', 'source', 'timestamp']); + .map((fieldItem) => fieldItem.prop('field').displayName) + ).toEqual(['amemory', 'bytes', 'client', 'source', 'timestampLabel']); }); it('should display spinner for available fields accordion if existing fields are not loaded yet', async () => { @@ -656,10 +669,9 @@ describe('IndexPattern Data Panel', () => { wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - 'bytes', - 'memory', - ]); + expect( + wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').displayName) + ).toEqual(['amemory', 'bytes']); }); it('should display no fields in groups when filtered by type Record', () => { @@ -686,14 +698,9 @@ describe('IndexPattern Data Panel', () => { .find('button') .first() .simulate('click'); - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - 'Records', - 'bytes', - 'memory', - 'client', - 'source', - 'timestamp', - ]); + expect( + wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').displayName) + ).toEqual(['Records', 'amemory', 'bytes', 'client', 'source', 'timestampLabel']); }); it('should filter down by type and by name', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index bdcce52314634a..0777b9b9d8e573 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -59,7 +59,7 @@ const FixedEuiContextMenuPanel = (EuiContextMenuPanel as unknown) as React.Funct >; function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { - return fieldA.name.localeCompare(fieldB.name, undefined, { sensitivity: 'base' }); + return fieldA.displayName.localeCompare(fieldB.displayName, undefined, { sensitivity: 'base' }); } const supportedFieldTypes = new Set(['string', 'number', 'boolean', 'date', 'ip', 'document']); @@ -323,7 +323,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ fieldGroup.filter((field) => { if ( localState.nameFilter.length && - !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) + !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) && + !field.displayName.toLowerCase().includes(localState.nameFilter.toLowerCase()) ) { return false; } 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 c6dbb6f617acfa..3d6c9f6047c813 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 @@ -8,6 +8,13 @@ import { mount } from 'enzyme'; import React from 'react'; import { BucketNestingEditor } from './bucket_nesting_editor'; import { IndexPatternColumn } from '../indexpattern'; +import { IndexPatternField } from '../types'; + +const fieldMap = { + a: { displayName: 'a' } as IndexPatternField, + b: { displayName: 'b' } as IndexPatternField, + c: { displayName: 'c' } as IndexPatternField, +}; describe('BucketNestingEditor', () => { function mockCol(col: Partial = {}): IndexPatternColumn { @@ -32,6 +39,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( void; + fieldMap: Record; }) { const column = layer.columns[columnId]; const columns = Object.entries(layer.columns); @@ -37,14 +39,14 @@ export function BucketNestingEditor({ .map(([value, c]) => ({ value, text: c.label, - fieldName: hasField(c) ? c.sourceField : '', + fieldName: hasField(c) ? fieldMap[c.sourceField].displayName : '', })); if (!column || !column.isBucketed || !aggColumns.length) { return null; } - const fieldName = hasField(column) ? column.sourceField : ''; + const fieldName = hasField(column) ? fieldMap[column.sourceField].displayName : ''; const prevColumn = layer.columnOrder[layer.columnOrder.indexOf(columnId) - 1]; 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 1f48f95ee45e09..bca179b437521a 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 @@ -45,6 +45,7 @@ const expectedIndexPatterns = { fields: [ { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -52,6 +53,7 @@ const expectedIndexPatterns = { }, { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -59,6 +61,7 @@ const expectedIndexPatterns = { }, { name: 'memory', + displayName: 'memory', type: 'number', aggregatable: true, searchable: true, @@ -66,6 +69,7 @@ const expectedIndexPatterns = { }, { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -210,9 +214,8 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(options).toHaveLength(2); expect(options![0].label).toEqual('Records'); - expect(options![1].options!.map(({ label }) => label)).toEqual([ - 'timestamp', + 'timestampLabel', 'bytes', 'memory', 'source', @@ -239,7 +242,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .filter('[data-test-subj="indexPattern-dimension-field"]') .prop('options'); - expect(options![1].options!.map(({ label }) => label)).toEqual(['timestamp', 'source']); + expect(options![1].options!.map(({ label }) => label)).toEqual(['timestampLabel', 'source']); }); it('should indicate fields which are incompatible for the operation of the current column', () => { @@ -277,7 +280,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] ).toContain('Incompatible'); expect( options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] @@ -651,7 +654,9 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(options![0]['data-test-subj']).toContain('Incompatible'); expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0][ + 'data-test-subj' + ] ).toContain('Incompatible'); expect( options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] @@ -769,7 +774,9 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(options![0]['data-test-subj']).toContain('Incompatible'); expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0][ + 'data-test-subj' + ] ).toContain('Incompatible'); expect( options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] @@ -923,7 +930,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(options![0]['data-test-subj']).toContain('Incompatible'); expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] ).toContain('Incompatible'); expect( options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] @@ -1095,12 +1102,12 @@ describe('IndexPatternDimensionEditorPanel', () => { columnOrder: ['col1'], columns: { col1: { - label: 'Average of bar', + label: 'Average of memory', dataType: 'number', isBucketed: false, // Private operationType: 'avg', - sourceField: 'bar', + sourceField: 'memory', }, }, }, @@ -1145,12 +1152,12 @@ describe('IndexPatternDimensionEditorPanel', () => { columnOrder: ['col1'], columns: { col1: { - label: 'Average of bar', + label: 'Average of memory', dataType: 'number', isBucketed: false, // Private operationType: 'avg', - sourceField: 'bar', + sourceField: 'memory', params: { format: { id: 'bytes', params: { decimals: 0 } }, }, @@ -1195,12 +1202,12 @@ describe('IndexPatternDimensionEditorPanel', () => { columnOrder: ['col1'], columns: { col1: { - label: 'Average of bar', + label: 'Average of memory', dataType: 'number', isBucketed: false, // Private operationType: 'avg', - sourceField: 'bar', + sourceField: 'memory', params: { format: { id: 'bytes', params: { decimals: 2 } }, }, @@ -1253,12 +1260,14 @@ describe('IndexPatternDimensionEditorPanel', () => { { aggregatable: true, name: 'bar', + displayName: 'bar', searchable: true, type: 'number', }, { aggregatable: true, name: 'mystring', + displayName: 'mystring', searchable: true, type: 'string', }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 4c85a55ad60119..b2a59788b50f9a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -74,7 +74,7 @@ export function FieldSelect({ function fieldNamesToOptions(items: string[]) { return items .map((field) => ({ - label: field, + label: fieldMap[field].displayName, value: { type: 'field', field, @@ -105,7 +105,7 @@ export function FieldSelect({ // eslint-disable-next-line @typescript-eslint/naming-convention 'lnFieldSelect__option--nonExistant': !exists, }), - 'data-test-subj': `lns-fieldOption${compatible ? '' : 'Incompatible'}-${label}`, + 'data-test-subj': `lns-fieldOption${compatible ? '' : 'Incompatible'}-${value.field}`, })); } @@ -161,7 +161,7 @@ export function FieldSelect({ ? selectedColumnSourceField ? [ { - label: selectedColumnSourceField, + label: fieldMap[selectedColumnSourceField].displayName, value: { type: 'field', field: selectedColumnSourceField }, }, ] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index a5108b30cea1da..038b51b9222869 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -378,6 +378,7 @@ export function PopoverEditor(props: PopoverEditorProps) { {!hideGrouping && ( { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts b/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts index e0a7f27835e42f..b0c5540a6b94f6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts @@ -5,12 +5,16 @@ */ import { i18n } from '@kbn/i18n'; +import { IndexPatternField } from './types'; /** * This is a special-case field which allows us to perform * document-level operations such as count. */ -export const documentField = { +export const documentField: IndexPatternField = { + displayName: i18n.translate('xpack.lens.indexPattern.records', { + defaultMessage: 'Records', + }), name: i18n.translate('xpack.lens.indexPattern.records', { defaultMessage: 'Records', }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 08a2f85ec70538..781222888b6dc0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -36,30 +36,35 @@ describe('IndexPattern Field Item', () => { fields: [ { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, }, { name: 'bytes', + displayName: 'bytesLabel', 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, @@ -83,6 +88,7 @@ describe('IndexPattern Field Item', () => { filters: [], field: { name: 'bytes', + displayName: 'bytesLabel', type: 'number', aggregatable: true, searchable: true, @@ -98,6 +104,13 @@ describe('IndexPattern Field Item', () => { } as unknown) as DataPublicPluginStart['fieldFormats']; }); + it('should display displayName of a field', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toEqual( + 'bytesLabel' + ); + }); + it('should request field stats without a time field, if the index pattern has none', async () => { indexPattern.timeFieldName = undefined; core.http.post.mockImplementationOnce(() => { @@ -149,6 +162,7 @@ describe('IndexPattern Field Item', () => { timeFieldName: 'timestamp', field: { name: 'bytes', + displayName: 'bytesLabel', type: 'number', aggregatable: true, searchable: true, @@ -235,6 +249,7 @@ describe('IndexPattern Field Item', () => { timeFieldName: 'timestamp', field: { name: 'bytes', + displayName: 'bytesLabel', type: 'number', aggregatable: true, searchable: true, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 5dc6673bc29ecd..5bcfbc64ec706c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -100,7 +100,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { isLoading: false, }); - const wrappableName = wrapOnDot(field.name)!; + const wrappableName = wrapOnDot(field.displayName)!; const wrappableHighlight = wrapOnDot(highlight); const highlightIndex = wrappableHighlight ? wrappableName.toLowerCase().indexOf(wrappableHighlight.toLowerCase()) @@ -204,7 +204,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { container={document.querySelector('.application') || undefined} button={ = { id: 'indexpattern', @@ -129,6 +131,7 @@ export function getIndexPatternDatasource({ savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, + indexPatternsService, }); }, @@ -209,9 +212,9 @@ export function getIndexPatternDatasource({ id, state, setState, - savedObjectsClient, onError: onIndexPatternLoadError, storage, + indexPatternsService, }); }} data={data} @@ -289,7 +292,6 @@ export function getIndexPatternDatasource({ { changeLayerIndexPattern({ - savedObjectsClient, indexPatternId, setState: props.setState, state: props.state, @@ -297,6 +299,7 @@ export function getIndexPatternDatasource({ onError: onIndexPatternLoadError, replaceIfPossible: true, storage, + indexPatternsService, }); }} {...props} 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 b6246c6e91e7e3..5489dcffc52c4e 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 @@ -23,36 +23,42 @@ const expectedIndexPatterns = { fields: [ { 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, @@ -66,6 +72,7 @@ const expectedIndexPatterns = { fields: [ { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -80,6 +87,7 @@ const expectedIndexPatterns = { }, { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -105,6 +113,7 @@ const expectedIndexPatterns = { }, { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -169,6 +178,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should apply a bucketed aggregation for a string field', () => { const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -214,6 +224,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should apply a bucketed aggregation for a date field', () => { const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -258,6 +269,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should select a metric for a number field', () => { const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -313,6 +325,7 @@ describe('IndexPattern Data Source suggestions', () => { fields: [ { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -331,6 +344,7 @@ describe('IndexPattern Data Source suggestions', () => { const suggestions = getDatasourceSuggestionsForField(state, '1', { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -374,6 +388,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should apply a bucketed aggregation for a string field', () => { const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -419,6 +434,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should apply a bucketed aggregation for a date field', () => { const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -463,6 +479,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should select a metric for a number field', () => { const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -518,6 +535,7 @@ describe('IndexPattern Data Source suggestions', () => { fields: [ { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -536,6 +554,7 @@ describe('IndexPattern Data Source suggestions', () => { const suggestions = getDatasourceSuggestionsForField(state, '1', { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -563,6 +582,7 @@ describe('IndexPattern Data Source suggestions', () => { it('creates a new layer and replaces layer if no match is found', () => { const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '2', { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -693,6 +713,7 @@ describe('IndexPattern Data Source suggestions', () => { '1', { name: 'start_date', + displayName: 'start_date', type: 'date', aggregatable: true, searchable: true, @@ -724,6 +745,7 @@ describe('IndexPattern Data Source suggestions', () => { const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -770,6 +792,7 @@ describe('IndexPattern Data Source suggestions', () => { it('does not use the same field for bucketing multiple times', () => { const suggestions = getDatasourceSuggestionsForField(stateWithNonEmptyTables(), '1', { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -785,6 +808,7 @@ describe('IndexPattern Data Source suggestions', () => { const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'dest', + displayName: 'dest', type: 'string', aggregatable: true, searchable: true, @@ -816,6 +840,7 @@ describe('IndexPattern Data Source suggestions', () => { const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'memory', + displayName: 'memory', type: 'number', aggregatable: true, searchable: true, @@ -858,6 +883,7 @@ describe('IndexPattern Data Source suggestions', () => { }; const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', { name: 'memory', + displayName: 'memory', type: 'number', aggregatable: true, searchable: true, @@ -908,6 +934,7 @@ describe('IndexPattern Data Source suggestions', () => { }; const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', { name: 'memory', + displayName: 'memory', type: 'number', aggregatable: true, searchable: true, @@ -961,6 +988,7 @@ describe('IndexPattern Data Source suggestions', () => { const initialState = stateWithCurrentIndexPattern(); const suggestions = getDatasourceSuggestionsForField(initialState, '2', { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -1015,6 +1043,7 @@ describe('IndexPattern Data Source suggestions', () => { const initialState = stateWithCurrentIndexPattern(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -1185,7 +1214,7 @@ describe('IndexPattern Data Source suggestions', () => { { columnId: 'id1', operation: { - label: 'timestamp', + label: 'timestampLabel', dataType: 'date', isBucketed: true, scale: 'interval', @@ -1261,7 +1290,7 @@ describe('IndexPattern Data Source suggestions', () => { { columnId: 'id1', operation: { - label: 'timestamp', + label: 'timestampLabel', dataType: 'date', isBucketed: true, scale: 'interval', @@ -1324,30 +1353,35 @@ describe('IndexPattern Data Source suggestions', () => { 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, @@ -1462,12 +1496,14 @@ describe('IndexPattern Data Source suggestions', () => { fields: [ { name: 'field1', + displayName: 'field1', type: 'number', aggregatable: true, searchable: true, }, { name: 'field2', + displayName: 'field2', type: 'date', aggregatable: true, searchable: true, @@ -1522,6 +1558,7 @@ describe('IndexPattern Data Source suggestions', () => { fields: [ { name: 'field1', + displayName: 'field1', type: 'number', aggregatable: true, searchable: true, 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 111a113a16be76..f3aa9c4f51c82f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -481,11 +481,19 @@ 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 firstBucketLabel = + currentFields.find((field) => field.name === layer.columns[firstBucket].sourceField) + ?.displayName || ''; + const secondBucketLabel = + currentFields.find((field) => field.name === layer.columns[secondBucket].sourceField) + ?.displayName || ''; + return buildSuggestion({ state, layerId, updatedLayer, - label: getNestedTitle([layer.columns[secondBucket], layer.columns[firstBucket]]), + label: getNestedTitle([secondBucketLabel, firstBucketLabel]), changeType: 'reorder', }); } @@ -544,12 +552,12 @@ function createMetricSuggestion( }); } -function getNestedTitle([outerBucket, innerBucket]: IndexPatternColumn[]) { +function getNestedTitle([outerBucketLabel, innerBucketLabel]: string[]) { return i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', { defaultMessage: '{innerOperation} for each {outerOperation}', values: { - innerOperation: innerBucket.sourceField, - outerOperation: outerBucket.sourceField, + innerOperation: innerBucketLabel, + outerOperation: outerBucketLabel, }, }); } 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 560c48b2155eef..738cdd611a7ba3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -65,30 +65,35 @@ const initialState: IndexPatternPrivateState = { 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, @@ -102,6 +107,7 @@ const initialState: IndexPatternPrivateState = { fields: [ { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', aggregatable: true, searchable: true, @@ -116,6 +122,7 @@ const initialState: IndexPatternPrivateState = { }, { name: 'bytes', + displayName: 'bytes', type: 'number', aggregatable: true, searchable: true, @@ -137,6 +144,7 @@ const initialState: IndexPatternPrivateState = { }, { name: 'source', + displayName: 'source', type: 'string', aggregatable: true, searchable: true, @@ -155,18 +163,21 @@ const initialState: IndexPatternPrivateState = { 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, 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 27904a0f23f166..cfabcb4edcef7e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -13,7 +13,14 @@ import { changeLayerIndexPattern, syncExistingFields, } from './loader'; -import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField } from './types'; +import { IndexPatternsContract } from '../../../../../src/plugins/data/public'; +import { + IndexPatternPersistedState, + IndexPatternPrivateState, + IndexPatternField, + IndexPattern, +} from './types'; +import { createMockedRestrictedIndexPattern, createMockedIndexPattern } from './mocks'; import { documentField } from './document_field'; jest.mock('./operations'); @@ -27,154 +34,157 @@ const createMockStorage = (lastData?: Record) => { }; }; -const sampleIndexPatterns = { - a: { - id: 'a', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - fields: [ - { - name: 'timestamp', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'start_date', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - type: 'string', - aggregatable: true, - searchable: true, - esTypes: ['keyword'], - }, - { - name: 'dest', - type: 'string', - aggregatable: true, - searchable: true, - esTypes: ['keyword'], - }, - documentField, - ], - }, - b: { - id: 'b', - title: 'my-fake-restricted-pattern', - timeFieldName: 'timestamp', - fields: [ - { - name: 'timestamp', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, +const indexPattern1 = ({ + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + 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, + esTypes: ['keyword'], + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + esTypes: ['keyword'], + }, + documentField, + ], +} as unknown) as IndexPattern; + +const sampleIndexPatternsFromService = { + '1': createMockedIndexPattern(), + '2': createMockedRestrictedIndexPattern(), +}; + +const indexPattern2 = ({ + id: '2', + title: 'my-fake-restricted-pattern', + 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', - 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: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + // Ignored in the UI + histogram: { + agg: 'histogram', + interval: 1000, }, - }, - { - name: 'source', - type: 'string', - aggregatable: false, - searchable: false, - scripted: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, + avg: { + agg: 'avg', + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', }, - esTypes: ['keyword'], }, - documentField, - ], - }, -}; - -function indexPatternSavedObject({ id }: { id: keyof typeof sampleIndexPatterns }) { - const pattern = { - ...sampleIndexPatterns[id], - fields: [ - ...sampleIndexPatterns[id].fields, - { - name: 'description', - type: 'string', - aggregatable: false, - searchable: true, - esTypes: ['text'], + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + scripted: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, }, - ], - }; - return { - id, - type: 'index-pattern', - attributes: { - title: pattern.title, - timeFieldName: pattern.timeFieldName, - fields: JSON.stringify(pattern.fields.filter((f) => f.type !== 'document')), + esTypes: ['keyword'], }, - }; -} + documentField, + ], +} as unknown) as IndexPattern; + +const sampleIndexPatterns = { + '1': indexPattern1, + '2': indexPattern2, +}; function mockClient() { return ({ find: jest.fn(async () => ({ savedObjects: [ - { id: 'a', attributes: { title: sampleIndexPatterns.a.title } }, - { id: 'b', attributes: { title: sampleIndexPatterns.b.title } }, + { id: '1', attributes: { title: sampleIndexPatterns[1].title } }, + { id: '2', attributes: { title: sampleIndexPatterns[2].title } }, ], })), - async bulkGet(indexPatterns: Array<{ id: keyof typeof sampleIndexPatterns }>) { - return { - savedObjects: indexPatterns.map(({ id }) => indexPatternSavedObject({ id })), - }; - }, - } as unknown) as Pick; + } as unknown) as Pick; +} + +function mockIndexPatternsService() { + return ({ + get: jest.fn(async (id: '1' | '2') => { + return sampleIndexPatternsFromService[id]; + }), + } as unknown) as Pick; } describe('loader', () => { @@ -182,11 +192,12 @@ describe('loader', () => { it('should not load index patterns that are already loaded', async () => { const cache = await loadIndexPatterns({ cache: sampleIndexPatterns, - patterns: ['a', 'b'], - savedObjectsClient: { - bulkGet: jest.fn(() => Promise.reject('bulkGet should not have been called')), - find: jest.fn(() => Promise.reject('find should not have been called')), - }, + patterns: ['1', '2'], + indexPatternsService: ({ + get: jest.fn(() => + Promise.reject('mockIndexPatternService.get should not have been called') + ), + } as unknown) as Pick, }); expect(cache).toEqual(sampleIndexPatterns); @@ -195,10 +206,10 @@ describe('loader', () => { it('should load index patterns that are not loaded', async () => { const cache = await loadIndexPatterns({ cache: { - b: sampleIndexPatterns.b, + '2': sampleIndexPatterns['2'], }, - patterns: ['a', 'b'], - savedObjectsClient: mockClient(), + patterns: ['1', '2'], + indexPatternsService: mockIndexPatternsService(), }); expect(cache).toMatchObject(sampleIndexPatterns); @@ -207,8 +218,8 @@ describe('loader', () => { it('should allow scripted, but not full text fields', async () => { const cache = await loadIndexPatterns({ cache: {}, - patterns: ['a', 'b'], - savedObjectsClient: mockClient(), + patterns: ['1', '2'], + indexPatternsService: mockIndexPatternsService(), }); expect(cache).toMatchObject(sampleIndexPatterns); @@ -218,61 +229,56 @@ describe('loader', () => { const cache = await loadIndexPatterns({ cache: {}, patterns: ['foo'], - savedObjectsClient: ({ - ...mockClient(), - async bulkGet() { - return { - savedObjects: [ - { - id: 'foo', - type: 'index-pattern', - attributes: { - title: 'Foo index', - typeMeta: JSON.stringify({ - aggs: { - date_histogram: { - timestamp: { - agg: 'date_histogram', - fixed_interval: 'm', - }, - }, - sum: { - bytes: { - agg: 'sum', - }, - }, - }, - }), - fields: JSON.stringify([ - { - name: 'timestamp', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ]), + indexPatternsService: ({ + get: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + typeMeta: { + aggs: { + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: 'm', }, }, - ], - }; - }, - } as unknown) as Pick, + sum: { + bytes: { + agg: 'sum', + }, + }, + }, + }, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + })), + } as unknown) as Pick, }); - expect(cache.foo.fields.find((f) => f.name === 'bytes')!.aggregationRestrictions).toEqual({ + expect( + cache.foo.fields.find((f: IndexPatternField) => f.name === 'bytes')!.aggregationRestrictions + ).toEqual({ sum: { agg: 'sum' }, }); - expect(cache.foo.fields.find((f) => f.name === 'timestamp')!.aggregationRestrictions).toEqual( - { - date_histogram: { agg: 'date_histogram', fixed_interval: 'm' }, - } - ); + expect( + cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')! + .aggregationRestrictions + ).toEqual({ + date_histogram: { agg: 'date_histogram', fixed_interval: 'm' }, + }); }); }); @@ -281,22 +287,23 @@ describe('loader', () => { const storage = createMockStorage(); const state = await loadInitialState({ savedObjectsClient: mockClient(), + indexPatternsService: mockIndexPatternsService(), storage, }); expect(state).toMatchObject({ - currentIndexPatternId: 'a', + currentIndexPatternId: '1', indexPatternRefs: [ - { id: 'a', title: sampleIndexPatterns.a.title }, - { id: 'b', title: sampleIndexPatterns.b.title }, + { id: '1', title: sampleIndexPatterns['1'].title }, + { id: '2', title: sampleIndexPatterns['2'].title }, ], indexPatterns: { - a: sampleIndexPatterns.a, + '1': sampleIndexPatterns['1'], }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: 'a', + indexPatternId: '1', }); }); @@ -304,39 +311,41 @@ describe('loader', () => { const storage = createMockStorage({ indexPatternId: 'c' }); const state = await loadInitialState({ savedObjectsClient: mockClient(), + indexPatternsService: mockIndexPatternsService(), storage, }); expect(state).toMatchObject({ - currentIndexPatternId: 'a', + currentIndexPatternId: '1', indexPatternRefs: [ - { id: 'a', title: sampleIndexPatterns.a.title }, - { id: 'b', title: sampleIndexPatterns.b.title }, + { id: '1', title: sampleIndexPatterns['1'].title }, + { id: '2', title: sampleIndexPatterns['2'].title }, ], indexPatterns: { - a: sampleIndexPatterns.a, + '1': sampleIndexPatterns['1'], }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: 'a', + indexPatternId: '1', }); }); it('should load lastUsedIndexPatternId if in localStorage', async () => { const state = await loadInitialState({ savedObjectsClient: mockClient(), - storage: createMockStorage({ indexPatternId: 'b' }), + indexPatternsService: mockIndexPatternsService(), + storage: createMockStorage({ indexPatternId: '2' }), }); expect(state).toMatchObject({ - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatternRefs: [ - { id: 'a', title: sampleIndexPatterns.a.title }, - { id: 'b', title: sampleIndexPatterns.b.title }, + { id: '1', title: sampleIndexPatterns['1'].title }, + { id: '2', title: sampleIndexPatterns['2'].title }, ], indexPatterns: { - b: sampleIndexPatterns.b, + '2': sampleIndexPatterns['2'], }, layers: {}, }); @@ -345,33 +354,34 @@ describe('loader', () => { it('should use the default index pattern id, if provided', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - defaultIndexPatternId: 'b', + defaultIndexPatternId: '2', savedObjectsClient: mockClient(), + indexPatternsService: mockIndexPatternsService(), storage, }); expect(state).toMatchObject({ - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatternRefs: [ - { id: 'a', title: sampleIndexPatterns.a.title }, - { id: 'b', title: sampleIndexPatterns.b.title }, + { id: '1', title: sampleIndexPatterns['1'].title }, + { id: '2', title: sampleIndexPatterns['2'].title }, ], indexPatterns: { - b: sampleIndexPatterns.b, + '2': sampleIndexPatterns['2'], }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: 'b', + indexPatternId: '2', }); }); it('should initialize from saved state', async () => { const savedState: IndexPatternPersistedState = { - currentIndexPatternId: 'b', + currentIndexPatternId: '2', layers: { layerb: { - indexPatternId: 'b', + indexPatternId: '2', columnOrder: ['col1', 'col2'], columns: { col1: { @@ -395,27 +405,28 @@ describe('loader', () => { }, }, }; - const storage = createMockStorage({ indexPatternId: 'a' }); + const storage = createMockStorage({ indexPatternId: '1' }); const state = await loadInitialState({ state: savedState, savedObjectsClient: mockClient(), + indexPatternsService: mockIndexPatternsService(), storage, }); expect(state).toMatchObject({ - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatternRefs: [ - { id: 'a', title: sampleIndexPatterns.a.title }, - { id: 'b', title: sampleIndexPatterns.b.title }, + { id: '1', title: sampleIndexPatterns['1'].title }, + { id: '2', title: sampleIndexPatterns['2'].title }, ], indexPatterns: { - b: sampleIndexPatterns.b, + '2': sampleIndexPatterns['2'], }, layers: savedState.layers, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: 'b', + indexPatternId: '2', }); }); }); @@ -424,33 +435,36 @@ describe('loader', () => { it('loads the index pattern and then sets it as current', async () => { const setState = jest.fn(); const state: IndexPatternPrivateState = { - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatternRefs: [], indexPatterns: {}, existingFields: {}, layers: {}, isFirstExistenceFetch: false, }; - const storage = createMockStorage({ indexPatternId: 'b' }); + const storage = createMockStorage({ indexPatternId: '2' }); await changeIndexPattern({ state, setState, - id: 'a', - savedObjectsClient: mockClient(), + id: '1', + indexPatternsService: mockIndexPatternsService(), onError: jest.fn(), storage, }); expect(setState).toHaveBeenCalledTimes(1); expect(setState.mock.calls[0][0](state)).toMatchObject({ - currentIndexPatternId: 'a', + currentIndexPatternId: '1', indexPatterns: { - a: sampleIndexPatterns.a, + '1': { + ...sampleIndexPatterns['1'], + fields: [...sampleIndexPatterns['1'].fields], + }, }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: 'a', + indexPatternId: '1', }); }); @@ -459,7 +473,7 @@ describe('loader', () => { const onError = jest.fn(); const err = new Error('NOPE!'); const state: IndexPatternPrivateState = { - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatternRefs: [], existingFields: {}, indexPatterns: {}, @@ -467,15 +481,14 @@ describe('loader', () => { isFirstExistenceFetch: false, }; - const storage = createMockStorage({ indexPatternId: 'b' }); + const storage = createMockStorage({ indexPatternId: '2' }); await changeIndexPattern({ state, setState, - id: 'a', - savedObjectsClient: { - ...mockClient(), - bulkGet: jest.fn(async () => { + id: '1', + indexPatternsService: { + get: jest.fn(async () => { throw err; }), }, @@ -493,17 +506,17 @@ describe('loader', () => { it('loads the index pattern and then changes the specified layer', async () => { const setState = jest.fn(); const state: IndexPatternPrivateState = { - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatternRefs: [], existingFields: {}, indexPatterns: { - a: sampleIndexPatterns.a, + '1': sampleIndexPatterns['1'], }, layers: { l0: { columnOrder: ['col1'], columns: {}, - indexPatternId: 'a', + indexPatternId: '1', }, l1: { columnOrder: ['col2'], @@ -519,36 +532,36 @@ describe('loader', () => { sourceField: 'timestamp', }, }, - indexPatternId: 'a', + indexPatternId: '1', }, }, isFirstExistenceFetch: false, }; - const storage = createMockStorage({ indexPatternId: 'a' }); + const storage = createMockStorage({ indexPatternId: '1' }); await changeLayerIndexPattern({ state, setState, - indexPatternId: 'b', + indexPatternId: '2', layerId: 'l1', - savedObjectsClient: mockClient(), + indexPatternsService: mockIndexPatternsService(), onError: jest.fn(), storage, }); expect(setState).toHaveBeenCalledTimes(1); expect(setState.mock.calls[0][0](state)).toMatchObject({ - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatterns: { - a: sampleIndexPatterns.a, - b: sampleIndexPatterns.b, + 1: sampleIndexPatterns['1'], + 2: sampleIndexPatterns['2'], }, layers: { l0: { columnOrder: ['col1'], columns: {}, - indexPatternId: 'a', + indexPatternId: '1', }, l1: { columnOrder: ['col2'], @@ -564,12 +577,12 @@ describe('loader', () => { sourceField: 'timestamp', }, }, - indexPatternId: 'b', + indexPatternId: '2', }, }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: 'b', + indexPatternId: '2', }); }); @@ -578,32 +591,31 @@ describe('loader', () => { const onError = jest.fn(); const err = new Error('NOPE!'); const state: IndexPatternPrivateState = { - currentIndexPatternId: 'b', + currentIndexPatternId: '2', indexPatternRefs: [], existingFields: {}, indexPatterns: { - a: sampleIndexPatterns.a, + '1': sampleIndexPatterns['1'], }, layers: { l0: { columnOrder: ['col1'], columns: {}, - indexPatternId: 'a', + indexPatternId: '1', }, }, isFirstExistenceFetch: false, }; - const storage = createMockStorage({ indexPatternId: 'b' }); + const storage = createMockStorage({ indexPatternId: '2' }); await changeLayerIndexPattern({ state, setState, - indexPatternId: 'b', + indexPatternId: '2', layerId: 'l0', - savedObjectsClient: { - ...mockClient(), - bulkGet: jest.fn(async () => { + indexPatternsService: { + get: jest.fn(async () => { throw err; }), }, @@ -634,7 +646,7 @@ describe('loader', () => { return { indexPatternTitle, existingFieldNames: ['field_1', 'field_2'].map( - (fieldName) => `${indexPatternTitle}_${fieldName}` + (fieldName) => `ip${indexPatternTitle}_${fieldName}` ), }; }) as unknown) as HttpHandler; @@ -643,9 +655,9 @@ describe('loader', () => { dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, fetchJson, indexPatterns: [ - { id: 'a', title: 'a', fields: [] }, - { id: 'b', title: 'a', fields: [] }, - { id: 'c', title: 'a', fields: [] }, + { id: '1', title: '1', fields: [] }, + { id: '2', title: '1', fields: [] }, + { id: '3', title: '1', fields: [] }, ], setState, dslQuery, @@ -668,9 +680,9 @@ describe('loader', () => { isFirstExistenceFetch: false, existenceFetchFailed: false, existingFields: { - a: { a_field_1: true, a_field_2: true }, - b: { b_field_1: true, b_field_2: true }, - c: { c_field_1: true, c_field_2: true }, + '1': { ip1_field_1: true, ip1_field_2: true }, + '2': { ip2_field_1: true, ip2_field_2: true }, + '3': { ip3_field_1: true, ip3_field_2: true }, }, }); }); @@ -683,7 +695,7 @@ describe('loader', () => { return { indexPatternTitle, existingFieldNames: - indexPatternTitle === 'a' + indexPatternTitle === '1' ? ['field_1', 'field_2'].map((fieldName) => `${indexPatternTitle}_${fieldName}`) : [], }; @@ -693,9 +705,9 @@ describe('loader', () => { dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, fetchJson, indexPatterns: [ - { id: 'a', title: 'a', fields: [] }, - { id: 'b', title: 'a', fields: [] }, - { id: 'c', title: 'a', fields: [] }, + { id: '1', title: '1', fields: [] }, + { id: '2', title: '1', fields: [] }, + { id: 'c', title: '1', fields: [] }, ], setState, dslQuery, @@ -725,8 +737,8 @@ describe('loader', () => { fetchJson, indexPatterns: [ { - id: 'a', - title: 'a', + id: '1', + title: '1', fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[], }, ], @@ -746,7 +758,7 @@ describe('loader', () => { }) as IndexPatternPrivateState; expect(newState.existenceFetchFailed).toEqual(true); - expect(newState.existingFields.a).toEqual({ + expect(newState.existingFields['1']).toEqual({ field1: true, field2: true, }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 20e7bec6db131f..9c4a19e58a0527 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -6,8 +6,7 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'kibana/public'; -import { SimpleSavedObject } from 'kibana/public'; +import { SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { StateSetter } from '../types'; import { IndexPattern, @@ -19,33 +18,25 @@ import { import { updateLayerIndexPattern } from './state_helpers'; import { DateRange, ExistingFields } from '../../common/types'; import { BASE_API_URL } from '../../common'; -import { documentField } from './document_field'; import { + IndexPatternsContract, indexPatterns as indexPatternsUtils, - IFieldType, - IndexPatternTypeMeta, } from '../../../../../src/plugins/data/public'; +import { documentField } from './document_field'; import { readFromStorage, writeToStorage } from '../settings_storage'; -interface SavedIndexPatternAttributes extends SavedObjectAttributes { - title: string; - timeFieldName: string | null; - fields: string; - fieldFormatMap: string; - typeMeta: string; -} - type SetState = StateSetter; -type SavedObjectsClient = Pick; +type SavedObjectsClient = Pick; +type IndexPatternsService = Pick; type ErrorHandler = (err: Error) => void; export async function loadIndexPatterns({ + indexPatternsService, patterns, - savedObjectsClient, cache, }: { + indexPatternsService: IndexPatternsService; patterns: string[]; - savedObjectsClient: SavedObjectsClient; cache: Record; }) { const missingIds = patterns.filter((id) => !cache[id]); @@ -54,20 +45,62 @@ export async function loadIndexPatterns({ return cache; } - const resp = await savedObjectsClient.bulkGet( - missingIds.map((id) => ({ id, type: 'index-pattern' })) - ); + const indexPatterns = await Promise.all(missingIds.map((id) => indexPatternsService.get(id))); + const indexPatternsObject = indexPatterns.reduce( + (acc, indexPattern) => { + const newFields = indexPattern.fields + .filter( + (field) => + !indexPatternsUtils.isNestedField(field) && (!!field.aggregatable || !!field.scripted) + ) + .map( + (field): IndexPatternField => ({ + name: field.name, + displayName: field.displayName, + type: field.type, + aggregatable: field.aggregatable, + searchable: field.searchable, + scripted: field.scripted, + esTypes: field.esTypes, + }) + ) + .concat(documentField); + + const { typeMeta, title, timeFieldName, fieldFormatMap } = indexPattern; + if (typeMeta?.aggs) { + const aggs = Object.keys(typeMeta.aggs); + newFields.forEach((field, index) => { + const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; + aggs.forEach((agg) => { + const restriction = + typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; + if (restriction) { + restrictionsObj[agg] = restriction; + } + }); + if (Object.keys(restrictionsObj).length) { + newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; + } + }); + } - return resp.savedObjects.reduce( - (acc, savedObject) => { - const indexPattern = fromSavedObject( - savedObject as SimpleSavedObject - ); - acc[indexPattern.id] = indexPattern; - return acc; + const currentIndexPattern: IndexPattern = { + id: indexPattern.id!, // id exists for sure because we got index patterns by id + title, + timeFieldName, + fieldFormatMap, + fields: newFields, + }; + + return { + [currentIndexPattern.id]: currentIndexPattern, + ...acc, + }; }, { ...cache } ); + + return indexPatternsObject; } const getLastUsedIndexPatternId = ( @@ -87,11 +120,13 @@ export async function loadInitialState({ savedObjectsClient, defaultIndexPatternId, storage, + indexPatternsService, }: { state?: IndexPatternPersistedState; savedObjectsClient: SavedObjectsClient; defaultIndexPatternId?: string; storage: IStorageWrapper; + indexPatternsService: IndexPatternsService; }): Promise { const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); @@ -108,7 +143,7 @@ export async function loadInitialState({ setLastUsedIndexPatternId(storage, currentIndexPatternId); const indexPatterns = await loadIndexPatterns({ - savedObjectsClient, + indexPatternsService, cache: {}, patterns: requiredPatterns, }); @@ -135,22 +170,22 @@ export async function loadInitialState({ export async function changeIndexPattern({ id, - savedObjectsClient, state, setState, onError, storage, + indexPatternsService, }: { id: string; - savedObjectsClient: SavedObjectsClient; state: IndexPatternPrivateState; setState: SetState; onError: ErrorHandler; storage: IStorageWrapper; + indexPatternsService: IndexPatternsService; }) { try { const indexPatterns = await loadIndexPatterns({ - savedObjectsClient, + indexPatternsService, cache: state.indexPatterns, patterns: [id], }); @@ -175,25 +210,25 @@ export async function changeIndexPattern({ export async function changeLayerIndexPattern({ indexPatternId, layerId, - savedObjectsClient, state, setState, onError, replaceIfPossible, storage, + indexPatternsService, }: { indexPatternId: string; layerId: string; - savedObjectsClient: SavedObjectsClient; state: IndexPatternPrivateState; setState: SetState; onError: ErrorHandler; replaceIfPossible?: boolean; storage: IStorageWrapper; + indexPatternsService: IndexPatternsService; }) { try { const indexPatterns = await loadIndexPatterns({ - savedObjectsClient, + indexPatternsService, cache: state.indexPatterns, patterns: [indexPatternId], }); @@ -319,55 +354,3 @@ function isSingleEmptyLayer(layerMap: IndexPatternPrivateState['layers']) { const layers = Object.values(layerMap); return layers.length === 1 && layers[0].columnOrder.length === 0; } - -function fromSavedObject( - savedObject: SimpleSavedObject -): IndexPattern { - const { id, attributes, type } = savedObject; - const indexPattern = { - ...attributes, - id, - type, - title: attributes.title, - fields: (JSON.parse(attributes.fields) as IFieldType[]) - .filter( - (field) => - !indexPatternsUtils.isNestedField(field) && (!!field.aggregatable || !!field.scripted) - ) - .concat(documentField) as IndexPatternField[], - typeMeta: attributes.typeMeta - ? (JSON.parse(attributes.typeMeta) as IndexPatternTypeMeta) - : undefined, - fieldFormatMap: attributes.fieldFormatMap ? JSON.parse(attributes.fieldFormatMap) : undefined, - }; - - const { typeMeta } = indexPattern; - if (!typeMeta) { - return indexPattern; - } - - const newFields = [...(indexPattern.fields as IndexPatternField[])]; - - if (typeMeta.aggs) { - const aggs = Object.keys(typeMeta.aggs); - newFields.forEach((field, index) => { - const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; - aggs.forEach((agg) => { - const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; - if (restriction) { - restrictionsObj[agg] = restriction; - } - }); - if (Object.keys(restrictionsObj).length) { - newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; - } - }); - } - - return { - id: indexPattern.id, - title: indexPattern.title, - timeFieldName: indexPattern.timeFieldName || undefined, - fields: newFields, - }; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index dff3e61342a6a6..869eee67d381dc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -14,39 +14,54 @@ export const createMockedIndexPattern = (): IndexPattern => ({ fields: [ { 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, + esTypes: ['keyword'], + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, }, { name: 'dest', + displayName: 'dest', type: 'string', aggregatable: true, searchable: true, + esTypes: ['keyword'], }, ], }); @@ -58,21 +73,26 @@ export const createMockedRestrictedIndexPattern = () => ({ 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, + scripted: true, + esTypes: ['keyword'], }, ], typeMeta: { 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 09faa4bb704472..ad04891b637d42 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 @@ -51,7 +51,7 @@ export const cardinalityOperation: OperationDefinition { return { ...oldColumn, - label: ofName(field.name), + label: ofName(field.displayName), sourceField: field.name, }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 1dcaf78b58a6c4..4e081da2c6dc90 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -26,7 +26,7 @@ export const countOperation: OperationDefinition = { onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, - label: field.name, + label: field.displayName, sourceField: field.name, }; }, 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 ebf8e09e863969..48a6079c58ac0a 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 @@ -58,6 +58,7 @@ describe('date_histogram', () => { fields: [ { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', esTypes: ['date'], aggregatable: true, @@ -71,6 +72,7 @@ describe('date_histogram', () => { fields: [ { name: 'other_timestamp', + displayName: 'other_timestamp', type: 'date', esTypes: ['date'], aggregatable: true, @@ -168,6 +170,7 @@ describe('date_histogram', () => { indexPattern: createMockedIndexPattern(), field: { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', esTypes: ['date'], aggregatable: true, @@ -185,6 +188,7 @@ describe('date_histogram', () => { indexPattern: createMockedIndexPattern(), field: { name: 'start_date', + displayName: 'start_date', type: 'date', esTypes: ['date'], aggregatable: true, @@ -202,6 +206,7 @@ describe('date_histogram', () => { indexPattern: createMockedIndexPattern(), field: { name: 'timestamp', + displayName: 'timestampLabel', type: 'date', esTypes: ['date'], aggregatable: true, @@ -298,6 +303,7 @@ describe('date_histogram', () => { fields: [ { name: 'dateField', + displayName: 'dateField', type: 'date', aggregatable: true, searchable: true, @@ -340,6 +346,7 @@ describe('date_histogram', () => { fields: [ { name: 'dateField', + displayName: 'dateField', type: 'date', aggregatable: true, searchable: true, 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 6e007c12acf425..2236bc576e2b66 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 @@ -64,7 +64,7 @@ export const dateHistogramOperation: OperationDefinition { return { ...oldColumn, - label: field.name, + label: field.displayName, sourceField: field.name, }; }, 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 3ede847a5e2571..e6c8a5f6ac8520 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 @@ -51,7 +51,7 @@ function buildMetricOperation>({ ); }, buildColumn: ({ suggestedPriority, field, previousColumn }) => ({ - label: ofName(field.name), + label: ofName(field.displayName), dataType: 'number', operationType: type, suggestedPriority, @@ -64,7 +64,7 @@ function buildMetricOperation>({ onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, - label: ofName(field.name), + label: ofName(field.displayName), sourceField: field.name, }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index d7f00e185a5bba..05bb2ef6738888 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -118,6 +118,7 @@ describe('terms', () => { aggregatable: true, searchable: true, name: 'test', + displayName: 'test', type: 'string', aggregationRestrictions: { terms: { @@ -136,6 +137,7 @@ describe('terms', () => { aggregatable: true, searchable: true, name: 'test', + displayName: 'test', type: 'number', aggregationRestrictions: { terms: { @@ -154,6 +156,7 @@ describe('terms', () => { aggregatable: true, searchable: true, name: 'test', + displayName: 'test', type: 'boolean', }) ).toEqual({ @@ -167,6 +170,7 @@ describe('terms', () => { aggregatable: true, searchable: true, name: 'test', + displayName: 'test', type: 'ip', }) ).toEqual({ @@ -182,6 +186,7 @@ describe('terms', () => { aggregatable: false, searchable: true, name: 'test', + displayName: 'test', type: 'string', }) ).toEqual(undefined); @@ -192,6 +197,7 @@ describe('terms', () => { aggregationRestrictions: {}, searchable: true, name: 'test', + displayName: 'test', type: 'string', }) ).toEqual(undefined); @@ -209,6 +215,7 @@ describe('terms', () => { searchable: true, type: 'boolean', name: 'test', + displayName: 'test', }, columns: {}, }); @@ -234,6 +241,7 @@ describe('terms', () => { searchable: true, type: 'boolean', name: 'test', + displayName: 'test', }, }); expect(termsColumn.params).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index 1ab58cb11c5983..ac1ff9da2fea08 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -79,7 +79,7 @@ export const termsOperation: OperationDefinition = { .map(([id]) => id)[0]; return { - label: ofName(field.name), + label: ofName(field.displayName), dataType: field.type as DataType, operationType: 'terms', scale: 'ordinal', @@ -115,7 +115,7 @@ export const termsOperation: OperationDefinition = { onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, - label: ofName(field.name), + label: ofName(field.displayName), sourceField: field.name, }; }, 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 1a37e5e4cf6a4f..3fce2562f528e0 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 @@ -19,18 +19,21 @@ const expectedIndexPatterns = { 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, @@ -46,6 +49,7 @@ describe('getOperationTypesForField', () => { getOperationTypesForField({ type: 'string', name: 'a', + displayName: 'aLabel', aggregatable: true, searchable: true, }) @@ -57,6 +61,7 @@ describe('getOperationTypesForField', () => { getOperationTypesForField({ type: 'number', name: 'a', + displayName: 'aLabel', aggregatable: true, searchable: true, }) @@ -68,6 +73,7 @@ describe('getOperationTypesForField', () => { getOperationTypesForField({ type: 'date', name: 'a', + displayName: 'aLabel', aggregatable: true, searchable: true, }) @@ -79,6 +85,7 @@ describe('getOperationTypesForField', () => { getOperationTypesForField({ type: '_source', name: 'a', + displayName: 'aLabel', aggregatable: true, searchable: true, }) @@ -92,6 +99,7 @@ describe('getOperationTypesForField', () => { getOperationTypesForField({ type: 'string', name: 'a', + displayName: 'aLabel', aggregatable: true, searchable: true, aggregationRestrictions: { @@ -108,6 +116,7 @@ describe('getOperationTypesForField', () => { getOperationTypesForField({ type: 'number', name: 'a', + displayName: 'aLabel', aggregatable: true, searchable: true, aggregationRestrictions: { @@ -127,6 +136,7 @@ describe('getOperationTypesForField', () => { getOperationTypesForField({ type: 'date', name: 'a', + displayName: 'aLabel', aggregatable: true, searchable: true, aggregationRestrictions: { 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 d778749ef3940f..d7fd0d3661c86e 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 @@ -573,12 +573,14 @@ describe('state_helpers', () => { fields: [ { name: 'fieldA', + displayName: 'fieldA', aggregatable: true, searchable: true, type: 'string', }, { name: 'fieldB', + displayName: 'fieldB', aggregatable: true, searchable: true, type: 'number', @@ -590,12 +592,14 @@ describe('state_helpers', () => { }, { name: 'fieldC', + displayName: 'fieldC', aggregatable: false, searchable: true, type: 'date', }, { name: 'fieldD', + displayName: 'fieldD', aggregatable: true, searchable: true, type: 'date', @@ -609,6 +613,7 @@ describe('state_helpers', () => { }, { name: 'fieldE', + displayName: 'fieldE', aggregatable: true, searchable: true, type: 'date', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 2a9b3f452d9913..8d0e82b176aa97 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -23,6 +23,7 @@ export interface IndexPattern { export interface IndexPatternField { name: string; + displayName: string; type: string; esTypes?: string[]; aggregatable: boolean; From 506bf6c76455eb2264e5c67c0cfa9ce2c64253f7 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 21 Aug 2020 12:10:50 +0200 Subject: [PATCH 05/77] [Uptime] Add delay in telemetry test (#75162) Co-authored-by: Elastic Machine --- .../apis/uptime/rest/telemetry_collectors.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts index f07ddf68152d3b..cf1e7ff9f0716d 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts @@ -118,9 +118,9 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should receive expected results after calling overview logging', async () => { + it('should receive 200 status after overview logging', async () => { // call overview page - const { body: result } = await supertest + await supertest .post(API_URLS.LOG_PAGE_VIEW) .set('kbn-xsrf', 'true') .send({ @@ -131,21 +131,6 @@ export default function ({ getService }: FtrProviderContext) { autoRefreshEnabled: true, }) .expect(200); - - expect(result).to.eql({ - overview_page: 1, - monitor_page: 1, - no_of_unique_monitors: 4, - settings_page: 0, - monitor_frequency: [120, 0.001, 60, 60], - monitor_name_stats: { min_length: 7, max_length: 22, avg_length: 12 }, - no_of_unique_observer_locations: 3, - observer_location_name_stats: { min_length: 2, max_length: 7, avg_length: 4.8 }, - dateRangeStart: ['now/d', 'now/d'], - dateRangeEnd: ['now/d', 'now-30'], - autoRefreshEnabled: true, - autorefreshInterval: [100, 60], - }); }); }); } From 7376e4ca3dcd0e40b132c55dcd379b1356522599 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 21 Aug 2020 12:20:46 +0200 Subject: [PATCH 06/77] [Console] Get ES Config from core (#75406) * Server side changes - removed console_legacy plugin! - added new es_config endpoint that returns server side es config at the moment this is just the first value in hosts - Slight refactor to how routes are registered to bring them more in line with other ES UI plugins * Client side update - Updated the client to not get es host from injected metadata. Instead use the new endpoint created server side that returns this value - Added a small README.md regarding the hooks lib and need to refactor use of jQuery in console - Write code to init the es host value on the client once at start up in a non-blocking way. If this fails we just use the default value of http://localhost:9200 as this powers non-essential console functionality (i.e., copy as cURL). * fix type issue and jest tests * fix another type issue * simplify proxy assignment in proxy handler mock Co-authored-by: Elastic Machine --- .../core_plugins/console_legacy/index.ts | 49 -------------- .../core_plugins/console_legacy/package.json | 4 -- .../console/common/types/api_responses.ts | 29 +++++++++ .../editor/legacy/console_editor/editor.tsx | 5 +- .../contexts/services_context.mock.ts | 8 ++- .../application/contexts/services_context.tsx | 30 +++++---- .../public/application/hooks/README.md | 5 ++ .../console/public/application/index.tsx | 11 ++-- .../console/public/application/lib/api.ts | 39 +++++++++++ .../public/application/lib/es_host_service.ts | 54 ++++++++++++++++ .../console/public/application/lib/index.ts | 21 ++++++ src/plugins/console/public/plugin.ts | 10 +-- src/plugins/console/public/shared_imports.ts | 22 +++++++ .../server/__tests__/proxy_route/mocks.ts | 46 +++++++++---- .../__tests__/proxy_route/params.test.ts | 8 ++- .../proxy_route/proxy_fallback.test.ts | 14 ++-- src/plugins/console/server/plugin.ts | 54 +++++++++------- .../routes/api/console/es_config/index.ts | 33 ++++++++++ .../api/console/proxy/create_handler.ts | 25 ++------ .../server/routes/api/console/proxy/index.ts | 16 ++--- .../api/console/spec_definitions/index.ts | 14 ++-- src/plugins/console/server/routes/index.ts | 50 +++++++++++++++ .../services/es_legacy_config_service.ts | 64 +++++++++++++++++++ src/plugins/console/server/services/index.ts | 2 + src/plugins/console/server/types.ts | 2 +- 25 files changed, 451 insertions(+), 164 deletions(-) delete mode 100644 src/legacy/core_plugins/console_legacy/index.ts delete mode 100644 src/legacy/core_plugins/console_legacy/package.json create mode 100644 src/plugins/console/common/types/api_responses.ts create mode 100644 src/plugins/console/public/application/hooks/README.md create mode 100644 src/plugins/console/public/application/lib/api.ts create mode 100644 src/plugins/console/public/application/lib/es_host_service.ts create mode 100644 src/plugins/console/public/application/lib/index.ts create mode 100644 src/plugins/console/public/shared_imports.ts create mode 100644 src/plugins/console/server/routes/api/console/es_config/index.ts create mode 100644 src/plugins/console/server/routes/index.ts create mode 100644 src/plugins/console/server/services/es_legacy_config_service.ts diff --git a/src/legacy/core_plugins/console_legacy/index.ts b/src/legacy/core_plugins/console_legacy/index.ts deleted file mode 100644 index 82e00a99c6cfdb..00000000000000 --- a/src/legacy/core_plugins/console_legacy/index.ts +++ /dev/null @@ -1,49 +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 { first } from 'rxjs/operators'; -import { head } from 'lodash'; -import url from 'url'; - -// TODO: Remove this hack once we can get the ES config we need for Console proxy a better way. -let _legacyEsConfig: any; -export const readLegacyEsConfig = () => { - return _legacyEsConfig; -}; - -// eslint-disable-next-line import/no-default-export -export default function (kibana: any) { - return new kibana.Plugin({ - id: 'console_legacy', - - async init(server: any) { - _legacyEsConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$ - .pipe(first()) - .toPromise(); - }, - - uiExports: { - injectDefaultVars: () => ({ - elasticsearchUrl: url.format( - Object.assign(url.parse(head(_legacyEsConfig.hosts) as any), { auth: false }) - ), - }), - }, - } as any); -} diff --git a/src/legacy/core_plugins/console_legacy/package.json b/src/legacy/core_plugins/console_legacy/package.json deleted file mode 100644 index b78807daed9598..00000000000000 --- a/src/legacy/core_plugins/console_legacy/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "console_legacy", - "version": "kibana" -} diff --git a/src/plugins/console/common/types/api_responses.ts b/src/plugins/console/common/types/api_responses.ts new file mode 100644 index 00000000000000..1c8166bbe27f2e --- /dev/null +++ b/src/plugins/console/common/types/api_responses.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. + */ + +export interface EsConfigApiResponse { + /** + * This is the first host in the hosts array that Kibana is configured to use + * to communicate with ES. + * + * At the moment this is used to power the copy as cURL functionality in Console + * to complete the host portion of the URL. + */ + host?: string; +} diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 880069d8ebc7ae..fc88b31711b23c 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -67,9 +67,8 @@ const inputId = 'ConAppInputTextarea'; function EditorUI({ initialTextValue }: EditorProps) { const { - services: { history, notifications, settings: settingsService }, + services: { history, notifications, settings: settingsService, esHostService }, docLinkVersion, - elasticsearchUrl, } = useServicesContext(); const { settings } = useEditorReadContext(); @@ -232,7 +231,7 @@ function EditorUI({ initialTextValue }: EditorProps) { { - return editorInstanceRef.current!.getRequestsAsCURL(elasticsearchUrl); + return editorInstanceRef.current!.getRequestsAsCURL(esHostService.getHost()); }} getDocumentation={() => { return getDocumentation(editorInstanceRef.current!, docLinkVersion); diff --git a/src/plugins/console/public/application/contexts/services_context.mock.ts b/src/plugins/console/public/application/contexts/services_context.mock.ts index ae8d15a890782a..ba982d3f50cfbd 100644 --- a/src/plugins/console/public/application/contexts/services_context.mock.ts +++ b/src/plugins/console/public/application/contexts/services_context.mock.ts @@ -17,21 +17,27 @@ * under the License. */ import { notificationServiceMock } from '../../../../../core/public/mocks'; +import { httpServiceMock } from '../../../../../core/public/mocks'; + import { HistoryMock } from '../../services/history.mock'; import { SettingsMock } from '../../services/settings.mock'; import { StorageMock } from '../../services/storage.mock'; +import { createApi, createEsHostService } from '../lib'; import { ContextValue } from './services_context'; export const serviceContextMock = { create: (): ContextValue => { const storage = new StorageMock({} as any, 'test'); + const http = httpServiceMock.createSetupContract(); + const api = createApi({ http }); + const esHostService = createEsHostService({ api }); (storage.keys as jest.Mock).mockImplementation(() => []); return { - elasticsearchUrl: 'test', services: { trackUiMetric: { count: () => {}, load: () => {} }, storage, + esHostService, settings: new SettingsMock(storage), history: new HistoryMock(storage), notifications: notificationServiceMock.createSetupContract(), diff --git a/src/plugins/console/public/application/contexts/services_context.tsx b/src/plugins/console/public/application/contexts/services_context.tsx index 3d4ac3291c5ac1..e2f01a152b27b7 100644 --- a/src/plugins/console/public/application/contexts/services_context.tsx +++ b/src/plugins/console/public/application/contexts/services_context.tsx @@ -17,22 +17,25 @@ * under the License. */ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useEffect } from 'react'; import { NotificationsSetup } from 'kibana/public'; -import { History, Storage, Settings } from '../../services'; +import { History, Settings, Storage } from '../../services'; import { ObjectStorageClient } from '../../../common/types'; import { MetricsTracker } from '../../types'; +import { EsHostService } from '../lib'; + +interface ContextServices { + history: History; + storage: Storage; + settings: Settings; + notifications: NotificationsSetup; + objectStorageClient: ObjectStorageClient; + trackUiMetric: MetricsTracker; + esHostService: EsHostService; +} export interface ContextValue { - services: { - history: History; - storage: Storage; - settings: Settings; - notifications: NotificationsSetup; - objectStorageClient: ObjectStorageClient; - trackUiMetric: MetricsTracker; - }; - elasticsearchUrl: string; + services: ContextServices; docLinkVersion: string; } @@ -44,6 +47,11 @@ interface ContextProps { const ServicesContext = createContext(null as any); export function ServicesContextProvider({ children, value }: ContextProps) { + useEffect(() => { + // Fire and forget, we attempt to init the host service once. + value.services.esHostService.init(); + }, [value.services.esHostService]); + return {children}; } diff --git a/src/plugins/console/public/application/hooks/README.md b/src/plugins/console/public/application/hooks/README.md new file mode 100644 index 00000000000000..10057193560e90 --- /dev/null +++ b/src/plugins/console/public/application/hooks/README.md @@ -0,0 +1,5 @@ +## Notes + +* Do not add any code directly to this directory. This code should be moved to the neighbouring `lib` directory to be in line with future ES UI plugin patterns. + +* The `es.send` method uses $.ajax under the hood and needs to be refactored to use the new platform-provided http client. \ No newline at end of file diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx index 051eaea27a7de6..0a5a502eb50623 100644 --- a/src/plugins/console/public/application/index.tsx +++ b/src/plugins/console/public/application/index.tsx @@ -19,19 +19,20 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { NotificationsSetup } from 'src/core/public'; +import { HttpSetup, NotificationsSetup } from 'src/core/public'; import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings } from '../services'; import * as localStorageObjectClient from '../lib/local_storage_object_client'; import { createUsageTracker } from '../services/tracker'; import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { createApi, createEsHostService } from './lib'; export interface BootDependencies { + http: HttpSetup; docLinkVersion: string; I18nContext: any; notifications: NotificationsSetup; - elasticsearchUrl: string; usageCollection?: UsageCollectionSetup; element: HTMLElement; } @@ -40,9 +41,9 @@ export function renderApp({ I18nContext, notifications, docLinkVersion, - elasticsearchUrl, usageCollection, element, + http, }: BootDependencies) { const trackUiMetric = createUsageTracker(usageCollection); trackUiMetric.load('opened_app'); @@ -54,14 +55,16 @@ export function renderApp({ const history = createHistory({ storage }); const settings = createSettings({ storage }); const objectStorageClient = localStorageObjectClient.create(storage); + const api = createApi({ http }); + const esHostService = createEsHostService({ api }); render( ; + +export const createApi = ({ http }: Dependencies) => { + return { + getEsConfig: () => { + return sendRequest(http, { + path: '/api/console/es_config', + method: 'get', + }); + }, + }; +}; diff --git a/src/plugins/console/public/application/lib/es_host_service.ts b/src/plugins/console/public/application/lib/es_host_service.ts new file mode 100644 index 00000000000000..887f270a246870 --- /dev/null +++ b/src/plugins/console/public/application/lib/es_host_service.ts @@ -0,0 +1,54 @@ +/* + * 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 { Api } from './api'; + +/** + * Very simple state for holding the current ES host. + * + * This is used to power the copy as cURL functionality. + */ +export class EsHostService { + private host = 'http://localhost:9200'; + + constructor(private readonly api: Api) {} + + private setHost(host: string): void { + this.host = host; + } + + /** + * Initialize the host value based on the value set on the server. + * + * This call is necessary because this value can only be retrieved at + * runtime. + */ + public async init() { + const { data } = await this.api.getEsConfig(); + if (data && data.host) { + this.setHost(data.host); + } + } + + public getHost(): string { + return this.host; + } +} + +export const createEsHostService = ({ api }: { api: Api }) => new EsHostService(api); diff --git a/src/plugins/console/public/application/lib/index.ts b/src/plugins/console/public/application/lib/index.ts new file mode 100644 index 00000000000000..1ba99cc6072698 --- /dev/null +++ b/src/plugins/console/public/application/lib/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { createApi, Api } from './api'; +export { createEsHostService, EsHostService } from './es_host_service'; diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 851dc7a063d7b4..03b65a8bd145c5 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -25,7 +25,7 @@ import { AppSetupUIPluginDependencies } from './types'; export class ConsoleUIPlugin implements Plugin { public setup( - { notifications, getStartServices }: CoreSetup, + { notifications, getStartServices, http }: CoreSetup, { devTools, home, usageCollection }: AppSetupUIPluginDependencies ) { home.featureCatalogue.register({ @@ -53,23 +53,17 @@ export class ConsoleUIPlugin implements Plugin ({ import { duration } from 'moment'; import { ProxyConfigCollection } from '../../lib'; -import { CreateHandlerDependencies } from '../../routes/api/console/proxy/create_handler'; -import { coreMock } from '../../../../../core/server/mocks'; +import { RouteDependencies, ProxyDependencies } from '../../routes'; +import { EsLegacyConfigService, SpecDefinitionsService } from '../../services'; +import { coreMock, httpServiceMock } from '../../../../../core/server/mocks'; -export const getProxyRouteHandlerDeps = ({ - proxyConfigCollection = new ProxyConfigCollection([]), - pathFilters = [/.*/], - readLegacyESConfig = () => ({ +const defaultProxyValue = Object.freeze({ + readLegacyESConfig: async () => ({ requestTimeout: duration(30000), customHeaders: {}, requestHeadersWhitelist: [], hosts: ['http://localhost:9200'], }), - log = coreMock.createPluginInitializerContext().logger.get(), -}: Partial): CreateHandlerDependencies => ({ - proxyConfigCollection, - pathFilters, - readLegacyESConfig, - log, + pathFilters: [/.*/], + proxyConfigCollection: new ProxyConfigCollection([]), }); + +interface MockDepsArgument extends Partial> { + proxy?: Partial; +} + +export const getProxyRouteHandlerDeps = ({ + proxy, + log = coreMock.createPluginInitializerContext().logger.get(), + router = httpServiceMock.createSetupContract().createRouter(), +}: MockDepsArgument): RouteDependencies => { + const services: RouteDependencies['services'] = { + esLegacyConfigService: new EsLegacyConfigService(), + specDefinitionService: new SpecDefinitionsService(), + }; + + return { + services, + router, + proxy: proxy + ? { + ...defaultProxyValue, + ...proxy, + } + : defaultProxyValue, + log, + }; +}; diff --git a/src/plugins/console/server/__tests__/proxy_route/params.test.ts b/src/plugins/console/server/__tests__/proxy_route/params.test.ts index 1ab9c3ae789cc1..e1c5295f6d30f0 100644 --- a/src/plugins/console/server/__tests__/proxy_route/params.test.ts +++ b/src/plugins/console/server/__tests__/proxy_route/params.test.ts @@ -36,7 +36,7 @@ describe('Console Proxy Route', () => { describe('no matches', () => { it('rejects with 403', async () => { handler = createHandler( - getProxyRouteHandlerDeps({ pathFilters: [/^\/foo\//, /^\/bar\//] }) + getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } }) ); const { status } = await handler( @@ -51,7 +51,7 @@ describe('Console Proxy Route', () => { describe('one match', () => { it('allows the request', async () => { handler = createHandler( - getProxyRouteHandlerDeps({ pathFilters: [/^\/foo\//, /^\/bar\//] }) + getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } }) ); (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo')); @@ -68,7 +68,9 @@ describe('Console Proxy Route', () => { }); describe('all match', () => { it('allows the request', async () => { - handler = createHandler(getProxyRouteHandlerDeps({ pathFilters: [/^\/foo\//] })); + handler = createHandler( + getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } }) + ); (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo')); diff --git a/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts index b226bad11a01af..fc5233d0f833d8 100644 --- a/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts +++ b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts @@ -38,12 +38,14 @@ describe('Console Proxy Route', () => { const handler = createHandler( getProxyRouteHandlerDeps({ - readLegacyESConfig: () => ({ - requestTimeout: duration(30000), - customHeaders: {}, - requestHeadersWhitelist: [], - hosts: ['http://localhost:9201', 'http://localhost:9202', 'http://localhost:9203'], - }), + proxy: { + readLegacyESConfig: async () => ({ + requestTimeout: duration(30000), + customHeaders: {}, + requestHeadersWhitelist: [], + hosts: ['http://localhost:9201', 'http://localhost:9202', 'http://localhost:9203'], + }), + }, }) ); diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index eedd1541e88981..a76a35f8146c90 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -19,13 +19,12 @@ import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { readLegacyEsConfig } from '../../../legacy/core_plugins/console_legacy'; - import { ProxyConfigCollection } from './lib'; -import { SpecDefinitionsService } from './services'; +import { SpecDefinitionsService, EsLegacyConfigService } from './services'; import { ConfigType } from './config'; -import { registerProxyRoute } from './routes/api/console/proxy'; -import { registerSpecDefinitionsRoute } from './routes/api/console/spec_definitions'; + +import { registerRoutes } from './routes'; + import { ESConfigForProxy, ConsoleSetup, ConsoleStart } from './types'; export class ConsoleServerPlugin implements Plugin { @@ -33,11 +32,13 @@ export class ConsoleServerPlugin implements Plugin { specDefinitionsService = new SpecDefinitionsService(); + esLegacyConfigService = new EsLegacyConfigService(); + constructor(private readonly ctx: PluginInitializerContext) { this.log = this.ctx.logger.get(); } - async setup({ http, capabilities, getStartServices }: CoreSetup) { + async setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { capabilities.registerProvider(() => ({ dev_tools: { show: true, @@ -46,30 +47,31 @@ export class ConsoleServerPlugin implements Plugin { })); const config = await this.ctx.config.create().pipe(first()).toPromise(); - - const { elasticsearch } = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); - + const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); + this.esLegacyConfigService.setup(elasticsearch.legacy.config$); + const router = http.createRouter(); - registerProxyRoute({ + registerRoutes({ + router, log: this.log, - proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig), - readLegacyESConfig: (): ESConfigForProxy => { - const legacyConfig = readLegacyEsConfig(); - return { - ...elasticsearch, - ...legacyConfig, - }; + services: { + esLegacyConfigService: this.esLegacyConfigService, + specDefinitionService: this.specDefinitionsService, + }, + proxy: { + proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig), + readLegacyESConfig: async (): Promise => { + const legacyConfig = await this.esLegacyConfigService.readConfig(); + return { + ...globalConfig.elasticsearch, + ...legacyConfig, + }; + }, + pathFilters: proxyPathFilters, }, - pathFilters: proxyPathFilters, - router, - }); - - registerSpecDefinitionsRoute({ - router, - services: { specDefinitions: this.specDefinitionsService }, }); return { @@ -82,4 +84,8 @@ export class ConsoleServerPlugin implements Plugin { ...this.specDefinitionsService.start(), }; } + + stop() { + this.esLegacyConfigService.stop(); + } } diff --git a/src/plugins/console/server/routes/api/console/es_config/index.ts b/src/plugins/console/server/routes/api/console/es_config/index.ts new file mode 100644 index 00000000000000..a115a6b32ad012 --- /dev/null +++ b/src/plugins/console/server/routes/api/console/es_config/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { EsConfigApiResponse } from '../../../../../common/types/api_responses'; +import { RouteDependencies } from '../../../'; + +export const registerEsConfigRoute = ({ router, services }: RouteDependencies): void => { + router.get({ path: '/api/console/es_config', validate: false }, async (ctx, req, res) => { + const { + hosts: [host], + } = await services.esLegacyConfigService.readConfig(); + + const body: EsConfigApiResponse = { host }; + + return res.ok({ body }); + }); +}; diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index a16fb1dadfbcf0..f6d9bcb77ddda9 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -21,7 +21,7 @@ import { Agent, IncomingMessage } from 'http'; import * as url from 'url'; import { pick, trimStart, trimEnd } from 'lodash'; -import { KibanaRequest, Logger, RequestHandler } from 'kibana/server'; +import { KibanaRequest, RequestHandler } from 'kibana/server'; import { ESConfigForProxy } from '../../../../types'; import { @@ -31,19 +31,14 @@ import { setHeaders, } from '../../../../lib'; -import { Body, Query } from './validation_config'; - // TODO: find a better way to get information from the request like remoteAddress and remotePort // for forwarding. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ensureRawRequest } from '../../../../../../../core/server/http/router'; -export interface CreateHandlerDependencies { - log: Logger; - readLegacyESConfig: () => ESConfigForProxy; - pathFilters: RegExp[]; - proxyConfigCollection: ProxyConfigCollection; -} +import { RouteDependencies } from '../../../'; + +import { Body, Query } from './validation_config'; function toURL(base: string, path: string) { const urlResult = new url.URL(`${trimEnd(base, '/')}/${trimStart(path, '/')}`); @@ -120,14 +115,8 @@ function getProxyHeaders(req: KibanaRequest) { export const createHandler = ({ log, - readLegacyESConfig, - pathFilters, - proxyConfigCollection, -}: CreateHandlerDependencies): RequestHandler => async ( - ctx, - request, - response -) => { + proxy: { readLegacyESConfig, pathFilters, proxyConfigCollection }, +}: RouteDependencies): RequestHandler => async (ctx, request, response) => { const { body, query } = request; const { path, method } = query; @@ -140,7 +129,7 @@ export const createHandler = ({ }); } - const legacyConfig = readLegacyESConfig(); + const legacyConfig = await readLegacyESConfig(); const { hosts } = legacyConfig; let esIncomingMessage: IncomingMessage; diff --git a/src/plugins/console/server/routes/api/console/proxy/index.ts b/src/plugins/console/server/routes/api/console/proxy/index.ts index 5f7df1d7cf66b3..5841671c340bdf 100644 --- a/src/plugins/console/server/routes/api/console/proxy/index.ts +++ b/src/plugins/console/server/routes/api/console/proxy/index.ts @@ -17,17 +17,13 @@ * under the License. */ -import { IRouter } from 'kibana/server'; import { routeValidationConfig } from './validation_config'; -import { createHandler, CreateHandlerDependencies } from './create_handler'; +import { createHandler } from './create_handler'; -export const registerProxyRoute = ( - deps: { - router: IRouter; - } & CreateHandlerDependencies -) => { - const { router, ...handlerDeps } = deps; - router.post( +import { RouteDependencies } from '../../../'; + +export const registerProxyRoute = (deps: RouteDependencies) => { + deps.router.post( { path: '/api/console/proxy', options: { @@ -39,6 +35,6 @@ export const registerProxyRoute = ( }, validate: routeValidationConfig, }, - createHandler(handlerDeps) + createHandler(deps) ); }; diff --git a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts index 5c7e679cd0d350..a179c36364e264 100644 --- a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts +++ b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { IRouter, RequestHandler } from 'kibana/server'; -import { SpecDefinitionsService } from '../../../../services'; +import { RequestHandler } from 'kibana/server'; +import { RouteDependencies } from '../../../'; interface SpecDefinitionsRouteResponse { es: { @@ -27,16 +27,10 @@ interface SpecDefinitionsRouteResponse { }; } -export const registerSpecDefinitionsRoute = ({ - router, - services, -}: { - router: IRouter; - services: { specDefinitions: SpecDefinitionsService }; -}) => { +export const registerSpecDefinitionsRoute = ({ router, services }: RouteDependencies) => { const handler: RequestHandler = async (ctx, request, response) => { const specResponse: SpecDefinitionsRouteResponse = { - es: services.specDefinitions.asJson(), + es: services.specDefinitionService.asJson(), }; return response.ok({ diff --git a/src/plugins/console/server/routes/index.ts b/src/plugins/console/server/routes/index.ts new file mode 100644 index 00000000000000..cbd1cef7b36e33 --- /dev/null +++ b/src/plugins/console/server/routes/index.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 { IRouter, Logger } from 'kibana/server'; + +import { EsLegacyConfigService, SpecDefinitionsService } from '../services'; +import { ESConfigForProxy } from '../types'; +import { ProxyConfigCollection } from '../lib'; + +import { registerEsConfigRoute } from './api/console/es_config'; +import { registerProxyRoute } from './api/console/proxy'; +import { registerSpecDefinitionsRoute } from './api/console/spec_definitions'; + +export interface ProxyDependencies { + readLegacyESConfig: () => Promise; + pathFilters: RegExp[]; + proxyConfigCollection: ProxyConfigCollection; +} + +export interface RouteDependencies { + router: IRouter; + log: Logger; + proxy: ProxyDependencies; + services: { + esLegacyConfigService: EsLegacyConfigService; + specDefinitionService: SpecDefinitionsService; + }; +} + +export const registerRoutes = (dependencies: RouteDependencies) => { + registerEsConfigRoute(dependencies); + registerProxyRoute(dependencies); + registerSpecDefinitionsRoute(dependencies); +}; diff --git a/src/plugins/console/server/services/es_legacy_config_service.ts b/src/plugins/console/server/services/es_legacy_config_service.ts new file mode 100644 index 00000000000000..37928839b1846f --- /dev/null +++ b/src/plugins/console/server/services/es_legacy_config_service.ts @@ -0,0 +1,64 @@ +/* + * 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 { Observable, Subscription } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { ElasticsearchConfig } from 'kibana/server'; + +export class EsLegacyConfigService { + /** + * The elasticsearch config value at a given point in time. + */ + private config?: ElasticsearchConfig; + + /** + * An observable that emits elasticsearch config. + */ + private config$?: Observable; + + /** + * A reference to the subscription to the elasticsearch observable + */ + private configSub?: Subscription; + + setup(config$: Observable) { + this.config$ = config$; + this.configSub = this.config$.subscribe((config) => { + this.config = config; + }); + } + + stop() { + if (this.configSub) { + this.configSub.unsubscribe(); + } + } + + async readConfig(): Promise { + if (!this.config$) { + throw new Error('Could not read elasticsearch config, this service has not been setup!'); + } + + if (!this.config) { + return this.config$.pipe(first()).toPromise(); + } + + return this.config; + } +} diff --git a/src/plugins/console/server/services/index.ts b/src/plugins/console/server/services/index.ts index c8dfeccd23070a..c9d0b8b8581505 100644 --- a/src/plugins/console/server/services/index.ts +++ b/src/plugins/console/server/services/index.ts @@ -17,4 +17,6 @@ * under the License. */ +export { EsLegacyConfigService } from './es_legacy_config_service'; + export { SpecDefinitionsService } from './spec_definitions_service'; diff --git a/src/plugins/console/server/types.ts b/src/plugins/console/server/types.ts index 4f026555ada7b7..5dc8322c23ea92 100644 --- a/src/plugins/console/server/types.ts +++ b/src/plugins/console/server/types.ts @@ -38,8 +38,8 @@ export interface ESConfigForProxy { requestTimeout: Duration; ssl?: { verificationMode: 'none' | 'certificate' | 'full'; - certificateAuthorities: string[] | string; alwaysPresentCertificate: boolean; + certificateAuthorities?: string[]; certificate?: string; key?: string; keyPassphrase?: string; From fd459dea5d884e883be6fe26747121e768a7844b Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 21 Aug 2020 14:35:31 +0200 Subject: [PATCH 07/77] Migrate CSP usage collector to `kibana_usage_collection` plugin (#75536) * move csp usage collector from legacy kibana plugin to kibana_usage_collection * make scripts/telemetry_check happy. * remove assertion on legacy kibana plugin * remove test on legacy kibana plugin * update README --- src/legacy/core_plugins/kibana/index.js | 6 ---- src/plugins/kibana_usage_collection/README.md | 1 + .../server/__snapshots__/index.test.ts.snap | 2 ++ .../collectors/csp}/csp_collector.test.ts | 29 ++++++------------- .../server/collectors/csp}/csp_collector.ts | 21 +++++++------- .../server/collectors/csp}/index.ts | 0 .../server/collectors/index.ts | 1 + .../kibana_usage_collection/server/plugin.ts | 12 ++++---- test/api_integration/apis/status/status.js | 4 --- test/functional/apps/status_page/index.ts | 8 ----- 10 files changed, 29 insertions(+), 55 deletions(-) rename src/{legacy/core_plugins/kibana/server/lib/csp_usage_collector => plugins/kibana_usage_collection/server/collectors/csp}/csp_collector.test.ts (79%) rename src/{legacy/core_plugins/kibana/server/lib/csp_usage_collector => plugins/kibana_usage_collection/server/collectors/csp}/csp_collector.ts (75%) rename src/{legacy/core_plugins/kibana/server/lib/csp_usage_collector => plugins/kibana_usage_collection/server/collectors/csp}/index.ts (100%) diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 2e30bc5ce05ee6..176c5386961a54 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -21,7 +21,6 @@ import Fs from 'fs'; import { promisify } from 'util'; import { getUiSettingDefaults } from './server/ui_setting_defaults'; -import { registerCspCollector } from './server/lib/csp_usage_collector'; const mkdirAsync = promisify(Fs.mkdir); @@ -53,10 +52,5 @@ export default function (kibana) { throw err; } }, - - init: async function (server) { - const { usageCollection } = server.newPlatform.setup.plugins; - registerCspCollector(usageCollection, server); - }, }); } diff --git a/src/plugins/kibana_usage_collection/README.md b/src/plugins/kibana_usage_collection/README.md index 6ef4f19c1570f6..73a4d53f305f25 100644 --- a/src/plugins/kibana_usage_collection/README.md +++ b/src/plugins/kibana_usage_collection/README.md @@ -7,3 +7,4 @@ This plugin registers the basic usage collectors from Kibana: - Ops stats - Number of Saved Objects per type - Non-default UI Settings +- CSP configuration 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 f07912eff02b7b..47a4c458a83985 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 @@ -9,3 +9,5 @@ exports[`kibana_usage_collection Runs the setup method without issues 3`] = `fal exports[`kibana_usage_collection Runs the setup method without issues 4`] = `false`; exports[`kibana_usage_collection Runs the setup method without issues 5`] = `false`; + +exports[`kibana_usage_collection Runs the setup method without issues 6`] = `true`; diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts similarity index 79% rename from src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts index 63c2cbec21b579..465b21e3578bab 100644 --- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts @@ -17,35 +17,24 @@ * under the License. */ -import { CspConfig, ICspConfig } from '../../../../../../core/server'; +import { CspConfig, ICspConfig } from '../../../../../core/server'; import { createCspCollector } from './csp_collector'; - -const createMockKbnServer = () => ({ - newPlatform: { - setup: { - core: { - http: { - csp: new CspConfig(), - }, - }, - }, - }, -}); +import { httpServiceMock } from '../../../../../core/server/mocks'; describe('csp collector', () => { - let kbnServer: ReturnType; + let httpMock: ReturnType; const mockCallCluster = null as any; function updateCsp(config: Partial) { - kbnServer.newPlatform.setup.core.http.csp = new CspConfig(config); + httpMock.csp = new CspConfig(config); } beforeEach(() => { - kbnServer = createMockKbnServer(); + httpMock = httpServiceMock.createSetupContract(); }); test('fetches whether strict mode is enabled', async () => { - const collector = createCspCollector(kbnServer as any); + const collector = createCspCollector(httpMock); expect((await collector.fetch(mockCallCluster)).strict).toEqual(true); @@ -54,7 +43,7 @@ describe('csp collector', () => { }); test('fetches whether the legacy browser warning is enabled', async () => { - const collector = createCspCollector(kbnServer as any); + const collector = createCspCollector(httpMock); expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(true); @@ -63,7 +52,7 @@ describe('csp collector', () => { }); test('fetches whether the csp rules have been changed or not', async () => { - const collector = createCspCollector(kbnServer as any); + const collector = createCspCollector(httpMock); expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(false); @@ -72,7 +61,7 @@ describe('csp collector', () => { }); test('does not include raw csp rules under any property names', async () => { - const collector = createCspCollector(kbnServer as any); + const collector = createCspCollector(httpMock); // It's important that we do not send the value of csp.rules here as it // can be customized with values that can be identifiable to given diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.ts similarity index 75% rename from src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts rename to src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.ts index 9c124a90e66eb4..c45a83588ee441 100644 --- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.ts @@ -17,12 +17,8 @@ * under the License. */ -import { Server } from 'hapi'; -import { CspConfig } from '../../../../../../core/server'; -import { - UsageCollectionSetup, - CollectorOptions, -} from '../../../../../../plugins/usage_collection/server'; +import { UsageCollectionSetup, CollectorOptions } from 'src/plugins/usage_collection/server'; +import { HttpServiceSetup, CspConfig } from '../../../../../core/server'; interface Usage { strict: boolean; @@ -30,12 +26,12 @@ interface Usage { rulesChangedFromDefault: boolean; } -export function createCspCollector(server: Server): CollectorOptions { +export function createCspCollector(http: HttpServiceSetup): CollectorOptions { return { type: 'csp', isReady: () => true, async fetch() { - const { strict, warnLegacyBrowsers, header } = server.newPlatform.setup.core.http.csp; + const { strict, warnLegacyBrowsers, header } = http.csp; return { strict, @@ -60,8 +56,11 @@ export function createCspCollector(server: Server): CollectorOptions { }; } -export function registerCspCollector(usageCollection: UsageCollectionSetup, server: Server): void { - const collectorConfig = createCspCollector(server); - const collector = usageCollection.makeUsageCollector(collectorConfig); +export function registerCspCollector( + usageCollection: UsageCollectionSetup, + http: HttpServiceSetup +): void { + const collectorOptions = createCspCollector(http); + const collector = usageCollection.makeUsageCollector(collectorOptions); usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/index.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/index.ts rename to src/plugins/kibana_usage_collection/server/collectors/csp/index.ts diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 1ca237528b41f3..1f9fe130fa45d2 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -22,3 +22,4 @@ export { registerManagementUsageCollector } from './management'; export { registerApplicationUsageCollector } from './application_usage'; export { registerKibanaUsageCollector } from './kibana'; export { registerOpsStatsCollector } from './ops_stats'; +export { registerCspCollector } from './csp'; diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 803a9146bd08fd..d4295c770803e1 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -37,6 +37,7 @@ import { registerManagementUsageCollector, registerOpsStatsCollector, registerUiMetricUsageCollector, + registerCspCollector, } from './collectors'; interface KibanaUsageCollectionPluginsDepsSetup { @@ -56,12 +57,9 @@ export class KibanaUsageCollectionPlugin implements Plugin { this.metric$ = new Subject(); } - public setup( - { savedObjects }: CoreSetup, - { usageCollection }: KibanaUsageCollectionPluginsDepsSetup - ) { - this.registerUsageCollectors(usageCollection, this.metric$, (opts) => - savedObjects.registerType(opts) + public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { + this.registerUsageCollectors(usageCollection, coreSetup, this.metric$, (opts) => + coreSetup.savedObjects.registerType(opts) ); } @@ -79,6 +77,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { private registerUsageCollectors( usageCollection: UsageCollectionSetup, + coreSetup: CoreSetup, metric$: Subject, registerType: SavedObjectsRegisterType ) { @@ -90,5 +89,6 @@ export class KibanaUsageCollectionPlugin implements Plugin { registerManagementUsageCollector(usageCollection, getUiSettingsClient); registerUiMetricUsageCollector(usageCollection, registerType, getSavedObjectsClient); registerApplicationUsageCollector(usageCollection, registerType, getSavedObjectsClient); + registerCspCollector(usageCollection, coreSetup.http); } } diff --git a/test/api_integration/apis/status/status.js b/test/api_integration/apis/status/status.js index c60d354090cc2a..edfd4ca08b34de 100644 --- a/test/api_integration/apis/status/status.js +++ b/test/api_integration/apis/status/status.js @@ -39,10 +39,6 @@ export default function ({ getService }) { expect(body.status.overall.state).to.be('green'); expect(body.status.statuses).to.be.an('array'); - const kibanaPlugin = body.status.statuses.find((s) => { - return s.id.indexOf('plugin:kibana') === 0; - }); - expect(kibanaPlugin.state).to.be('green'); expect(body.metrics.collection_interval_in_millis).to.be.a('number'); diff --git a/test/functional/apps/status_page/index.ts b/test/functional/apps/status_page/index.ts index 65349aba93b9bc..234e61a142a818 100644 --- a/test/functional/apps/status_page/index.ts +++ b/test/functional/apps/status_page/index.ts @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const retry = getService('retry'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common']); @@ -32,13 +31,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('status_page'); }); - it('should show the kibana plugin as ready', async () => { - await retry.tryForTime(6000, async () => { - const text = await testSubjects.getVisibleText('statusBreakdown'); - expect(text.indexOf('plugin:kibana')).to.be.above(-1); - }); - }); - it('should show the build hash and number', async () => { const buildNumberText = await testSubjects.getVisibleText('statusBuildNumber'); expect(buildNumberText).to.contain('BUILD '); From ee75e571ad6842d25eab5572f42e7ec0482b5a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 21 Aug 2020 13:47:01 +0100 Subject: [PATCH 08/77] [Data Telemetry] Add index pattern to identify "meow" attacks (#75163) Co-authored-by: Elastic Machine --- .../telemetry_collection/get_data_telemetry/constants.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts index 2d0864b1cb75f8..7e4176281db41f 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts @@ -128,6 +128,9 @@ export const DATA_DATASETS_INDEX_PATTERNS = [ { pattern: '*suricata*', patternName: 'suricata' }, // { pattern: '*fsf*', patternName: 'fsf' }, // Disabled because it's too vague { pattern: '*wazuh*', patternName: 'wazuh' }, + + // meow attacks + { pattern: '*meow*', patternName: 'meow' }, ] as const; // Get the unique list of index patterns (some are duplicated for documentation purposes) From da0da4ca752f01428e126964039b0ac867580c21 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Fri, 21 Aug 2020 09:43:42 -0400 Subject: [PATCH 09/77] [Security Solution] modify circular deps checker to output images of circular deps graphs (#75579) --- .../run_check_circular_deps_cli.js | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js b/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js index 9b4a57f09066d1..ac4102184091dd 100644 --- a/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; - /* eslint-disable-next-line import/no-extraneous-dependencies */ import madge from 'madge'; /* eslint-disable-next-line import/no-extraneous-dependencies */ import { run, createFailError } from '@kbn/dev-utils'; +import * as os from 'os'; +import * as path from 'path'; run( - async ({ log }) => { + async ({ log, flags }) => { const result = await madge( - [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], + [path.resolve(__dirname, '../../public'), path.resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], excludeRegExp: [ @@ -34,6 +34,13 @@ run( const circularFound = result.circular(); if (circularFound.length !== 0) { + if (flags.svg) { + await outputSVGs(circularFound); + } else { + console.log( + 'Run this program with the --svg flag to save an SVG showing the dependency graph.' + ); + } throw createFailError( `SIEM circular dependencies of imports has been found:\n - ${circularFound.join('\n - ')}` ); @@ -42,6 +49,34 @@ run( } }, { - description: 'Check the SIEM plugin for circular deps', + description: + 'Check the Security Solution plugin for circular deps. If any are found, this will throw an Error.', + flags: { + help: ' --svg, Output SVGs of circular dependency graphs', + boolean: ['svg'], + default: { + svg: false, + }, + }, } ); + +async function outputSVGs(circularFound) { + let count = 0; + for (const found of circularFound) { + // Calculate the path using the os tmpdir and an increasing 'count' + const expectedImagePath = path.join(os.tmpdir(), `security_solution-circular-dep-${count}.svg`); + console.log(`Attempting to save SVG for circular dependency: ${found}`); + count++; + + // Graph just the files in the found circular dependency. + const specificGraph = await madge(found, { + fileExtensions: ['ts', 'js', 'tsx'], + }); + + // Output an SVG in the tmp directory + const imagePath = await specificGraph.image(expectedImagePath); + + console.log(`Saved SVG: ${imagePath}`); + } +} From c68363995b5fc0ce6820c5cdd92ab0d8d6d29d58 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 21 Aug 2020 16:15:12 +0200 Subject: [PATCH 10/77] Improve login UI error message. (#75642) --- .../components/login_form/login_form.test.tsx | 49 +++++++++++++++++++ .../components/login_form/login_form.tsx | 14 ++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx index 552d523fa4a84b..b6dd06595ae7f7 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx @@ -421,9 +421,58 @@ describe('LoginForm', () => { expect(window.location.href).toBe(currentURL); expect(coreStartMock.notifications.toasts.addError).toHaveBeenCalledWith(failureReason, { title: 'Could not perform login.', + toastMessage: 'Oh no!', }); }); + it('shows error with message in the `body`', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.post.mockRejectedValue({ + body: { message: 'Oh no! But with much more details!' }, + message: 'Oh no!', + }); + + window.location.href = currentURL; + const wrapper = mountWithIntl( + + ); + + expectPageMode(wrapper, PageMode.Selector); + + wrapper.findWhere((node) => node.key() === 'saml1').simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith('/internal/security/login', { + body: JSON.stringify({ providerType: 'saml', providerName: 'saml1', currentURL }), + }); + + expect(window.location.href).toBe(currentURL); + expect(coreStartMock.notifications.toasts.addError).toHaveBeenCalledWith( + new Error('Oh no! But with much more details!'), + { title: 'Could not perform login.', toastMessage: 'Oh no!' } + ); + }); + it('properly switches to login form', async () => { const currentURL = `https://some-host/login?next=${encodeURIComponent( '/some-base-path/app/kibana#/home?_g=()' diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index 9ea553af75e000..a929b50fa1ffa5 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -451,11 +451,15 @@ export class LoginForm extends Component { window.location.href = location; } catch (err) { - this.props.notifications.toasts.addError(err, { - title: i18n.translate('xpack.security.loginPage.loginSelectorErrorMessage', { - defaultMessage: 'Could not perform login.', - }), - }); + this.props.notifications.toasts.addError( + err?.body?.message ? new Error(err?.body?.message) : err, + { + title: i18n.translate('xpack.security.loginPage.loginSelectorErrorMessage', { + defaultMessage: 'Could not perform login.', + }), + toastMessage: err?.message, + } + ); this.setState({ loadingState: { type: LoadingStateType.None } }); } From 6b3ce3f91ee53c9050401af1edb80bd8793cf1c4 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 21 Aug 2020 10:18:14 -0400 Subject: [PATCH 11/77] [Dashboard First] Lens Originating App Breadcrumb (#75470) Changed lens breadcrumbs to reflect the Originating App --- .../lens/public/app_plugin/app.test.tsx | 34 +++++++++++++++++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 21 +++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index f92343183a700e..70136a486e8c1a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -136,6 +136,7 @@ describe('Lens App', () => { originatingApp: string | undefined; onAppLeave: AppMountParameters['onAppLeave']; history: History; + getAppNameFromId?: (appId: string) => string | undefined; }> { return ({ navigation: navigationStartMock, @@ -187,6 +188,7 @@ describe('Lens App', () => { originatingApp: string | undefined; onAppLeave: AppMountParameters['onAppLeave']; history: History; + getAppNameFromId?: (appId: string) => string | undefined; }>; } @@ -298,6 +300,38 @@ describe('Lens App', () => { ]); }); + it('sets originatingApp breadcrumb when the document title changes', async () => { + const defaultArgs = makeDefaultArgs(); + defaultArgs.originatingApp = 'ultraCoolDashboard'; + defaultArgs.getAppNameFromId = () => 'The Coolest Container Ever Made'; + instance = mount(); + + expect(core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Create' }, + ]); + + (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'Daaaaaaadaumching!', + expression: 'valid expression', + state: { + query: 'fake query', + datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + }, + }); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + expect(defaultArgs.core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Daaaaaaadaumching!' }, + ]); + }); + describe('persistence', () => { it('does not load a document if there is no document id', () => { const args = makeDefaultArgs(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index b20fe2f8046835..5ca6f27a0c578d 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { AppMountContext, AppMountParameters, NotificationsStart } from 'kibana/public'; import { History } from 'history'; +import { EuiBreadcrumb } from '@elastic/eui'; import { Query, DataPublicPluginStart, @@ -203,6 +204,16 @@ export function App({ // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { core.chrome.setBreadcrumbs([ + ...(originatingApp && getAppNameFromId + ? [ + { + onClick: (e) => { + core.application.navigateToApp(originatingApp); + }, + text: getAppNameFromId(originatingApp), + } as EuiBreadcrumb, + ] + : []), { href: core.http.basePath.prepend(`/app/visualize#/`), onClick: (e) => { @@ -219,7 +230,15 @@ export function App({ : i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create' }), }, ]); - }, [core.application, core.chrome, core.http.basePath, state.persistedDoc]); + }, [ + core.application, + core.chrome, + core.http.basePath, + state.persistedDoc, + originatingApp, + redirectTo, + getAppNameFromId, + ]); useEffect( () => { From 338b61ce6c1643af23b42e9a130085b0a55e8e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 21 Aug 2020 15:23:39 +0100 Subject: [PATCH 12/77] [Usage Collection Schemas] Remove Legacy entries (#75652) --- .telemetryrc.json | 9 --------- .../telemetry/schema/legacy_oss_plugins.json | 17 ----------------- 2 files changed, 26 deletions(-) delete mode 100644 src/plugins/telemetry/schema/legacy_oss_plugins.json diff --git a/.telemetryrc.json b/.telemetryrc.json index 30643a104c1cd2..2f57566159a700 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -1,13 +1,4 @@ [ - { - "output": "src/plugins/telemetry/schema/legacy_oss_plugins.json", - "root": "src/legacy/core_plugins/", - "exclude": [ - "src/legacy/core_plugins/testbed", - "src/legacy/core_plugins/elasticsearch", - "src/legacy/core_plugins/tests_bundle" - ] - }, { "output": "src/plugins/telemetry/schema/oss_plugins.json", "root": "src/plugins/", diff --git a/src/plugins/telemetry/schema/legacy_oss_plugins.json b/src/plugins/telemetry/schema/legacy_oss_plugins.json deleted file mode 100644 index e660ccac9dc36b..00000000000000 --- a/src/plugins/telemetry/schema/legacy_oss_plugins.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "properties": { - "csp": { - "properties": { - "strict": { - "type": "boolean" - }, - "warnLegacyBrowsers": { - "type": "boolean" - }, - "rulesChangedFromDefault": { - "type": "boolean" - } - } - } - } -} From 172c464b147bb207b71530fe9c82170ba5d3893f Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 21 Aug 2020 09:02:15 -0700 Subject: [PATCH 13/77] [Enterprise Search] Convert our `public_url` route to `config_data` and collect initialAppData (#75616) * [Setup] DRY out stripTrailingSlash helper - DRYs out repeated code - This will be used by an upcoming server/ endpoint change, hence why it's in common * [Setup] DRY out initial app data types to common/types - In preparation for upcoming server logic that will need to reuse these types + DRY out and clean up workplace_search types - remove unused supportEligible - remove currentUser - unneeded in Kibana * Update callEnterpriseSearchConfigAPI to parse and fetch new expected data * Remove /public_url API for /config_data * Remove getPublicUrl in favor of directly calling the new /config_data API from public/plugin + set returned initialData in this.data * Set up product apps to be passed initial data as props * Fix for Kea/redux state not resetting between AS<->WS nav - resetContext at the top level only gets called once total on first plugin load and never after, causing navigating between WS and AS to crash when both have Kea - this fixes the issue - moves redux Provider to top level app as well * Add very basic Kea logic file to App Search * Finish AppSearchConfigured tests & set up kea+useEffect mocks * [Cleanup] DRY out repeated mock initialAppData to a reusable defaults constant --- .../common/__mocks__/index.ts | 7 ++ .../common/__mocks__/initial_app_data.ts | 45 +++++++++ .../common/strip_trailing_slash/index.test.ts | 17 ++++ .../common/strip_trailing_slash/index.ts | 13 +++ .../common/types/app_search.ts | 25 +++++ .../enterprise_search/common/types/index.ts | 24 +++++ .../common/types/workplace_search.ts | 19 ++++ .../public/applications/__mocks__/kea.mock.ts | 24 +++++ .../__mocks__/shallow_usecontext.mock.ts | 1 + .../applications/app_search/app_logic.test.ts | 35 +++++++ .../applications/app_search/app_logic.ts | 31 ++++++ .../applications/app_search/index.test.tsx | 53 ++++++++-- .../public/applications/app_search/index.tsx | 41 +++++--- .../public/applications/index.tsx | 27 ++++-- .../get_enterprise_search_url.test.ts | 30 ------ .../get_enterprise_search_url.ts | 27 ------ .../shared/enterprise_search_url/index.ts | 1 - .../applications/shared/layout/side_nav.tsx | 3 +- .../overview/__mocks__/overview_logic.mock.ts | 3 +- .../overview/onboarding_steps.test.tsx | 1 - .../overview/overview_logic.test.ts | 9 +- .../components/overview/overview_logic.ts | 9 +- .../applications/workplace_search/index.tsx | 48 ++++------ .../applications/workplace_search/types.ts | 20 +--- .../enterprise_search/public/plugin.ts | 21 ++-- .../lib/enterprise_search_config_api.test.ts | 96 ++++++++++++++++++- .../lib/enterprise_search_config_api.ts | 53 +++++++++- .../enterprise_search/server/plugin.ts | 4 +- ...public_url.test.ts => config_data.test.ts} | 39 +++++--- .../routes/enterprise_search/config_data.ts | 32 +++++++ .../routes/enterprise_search/public_url.ts | 26 ----- 31 files changed, 573 insertions(+), 211 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/common/__mocks__/index.ts create mode 100644 x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts create mode 100644 x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts create mode 100644 x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts create mode 100644 x-pack/plugins/enterprise_search/common/types/app_search.ts create mode 100644 x-pack/plugins/enterprise_search/common/types/index.ts create mode 100644 x-pack/plugins/enterprise_search/common/types/workplace_search.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts rename x-pack/plugins/enterprise_search/server/routes/enterprise_search/{public_url.test.ts => config_data.test.ts} (50%) create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts delete mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/index.ts b/x-pack/plugins/enterprise_search/common/__mocks__/index.ts new file mode 100644 index 00000000000000..57029913fe3a99 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/__mocks__/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './initial_app_data'; diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts new file mode 100644 index 00000000000000..79e1efc425b4e2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DEFAULT_INITIAL_APP_DATA = { + readOnlyMode: false, + ilmEnabled: true, + configuredLimits: { + maxDocumentByteSize: 102400, + maxEnginesPerMetaEngine: 15, + }, + appSearch: { + accountId: 'some-id-string', + onBoardingComplete: true, + role: { + id: 'account_id:somestring|user_oid:somestring', + roleType: 'owner', + ability: { + accessAllEngines: true, + destroy: ['session'], + manage: ['account_credentials', 'account_engines'], // etc + edit: ['LocoMoco::Account'], // etc + view: ['Engine'], // etc + credentialTypes: ['admin', 'private', 'search'], + availableRoleTypes: ['owner', 'admin'], + }, + }, + }, + workplaceSearch: { + organization: { + name: 'ACME Donuts', + defaultOrgName: 'My Organization', + }, + fpAccount: { + id: 'some-id-string', + groups: ['Default', 'Cats'], + isAdmin: true, + canCreatePersonalSources: true, + isCurated: false, + viewedOnboardingPage: true, + }, + }, +}; diff --git a/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts new file mode 100644 index 00000000000000..b5d64455b1a90d --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { stripTrailingSlash } from './'; + +describe('Strip Trailing Slash helper', () => { + it('strips trailing slashes', async () => { + expect(stripTrailingSlash('http://trailing.slash/')).toEqual('http://trailing.slash'); + }); + + it('does nothing is there is no trailing slash', async () => { + expect(stripTrailingSlash('http://ok.url')).toEqual('http://ok.url'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts new file mode 100644 index 00000000000000..ade9bd8742c971 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Small helper for stripping trailing slashes from URLs or paths + * (usually ones that come in from React Router or API endpoints) + */ +export const stripTrailingSlash = (url: string): string => { + return url && url.endsWith('/') ? url.slice(0, -1) : url; +}; diff --git a/x-pack/plugins/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts new file mode 100644 index 00000000000000..5d6ec079e66e01 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/app_search.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface IAccount { + accountId: string; + onBoardingComplete: boolean; + role: IRole; +} + +export interface IRole { + id: string; + roleType: string; + ability: { + accessAllEngines: boolean; + destroy: string[]; + manage: string[]; + edit: string[]; + view: string[]; + credentialTypes: string[]; + availableRoleTypes: string[]; + }; +} diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts new file mode 100644 index 00000000000000..52e468b741a071 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { IAccount as IAppSearchAccount } from './app_search'; +import { IAccount as IWorkplaceSearchAccount, IOrganization } from './workplace_search'; + +export interface IInitialAppData { + readOnlyMode?: boolean; + ilmEnabled?: boolean; + configuredLimits?: IConfiguredLimits; + appSearch?: IAppSearchAccount; + workplaceSearch?: { + organization: IOrganization; + fpAccount: IWorkplaceSearchAccount; + }; +} + +export interface IConfiguredLimits { + maxDocumentByteSize: number; + maxEnginesPerMetaEngine: number; +} diff --git a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts new file mode 100644 index 00000000000000..fd8fa6daf81acd --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface IAccount { + id: string; + groups: string[]; + isAdmin: boolean; + isCurated: boolean; + canCreatePersonalSources: boolean; + viewedOnboardingPage: boolean; +} + +export interface IOrganization { + name: string; + defaultOrgName: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts new file mode 100644 index 00000000000000..5049e9da21ce9b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +jest.mock('kea', () => ({ + ...(jest.requireActual('kea') as object), + useValues: jest.fn(() => ({})), + useActions: jest.fn(() => ({})), +})); + +/** + * Example usage within a component test: + * + * import '../../../__mocks__/kea'; // Must come before kea's import, adjust relative path as needed + * + * import { useActions, useValues } from 'kea'; + * + * it('some test', () => { + * (useValues as jest.Mock).mockImplementationOnce(() => ({ someValue: 'hello' })); + * (useActions as jest.Mock).mockImplementationOnce(() => ({ someAction: () => 'world' })); + * }); + */ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts index 792be49a49c480..3a2193db646de4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts @@ -14,6 +14,7 @@ import { mockLicenseContext } from './license_context.mock'; jest.mock('react', () => ({ ...(jest.requireActual('react') as object), useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })), + useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior })); /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts new file mode 100644 index 00000000000000..bc31b7df5d971d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { resetContext } from 'kea'; + +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; +import { AppLogic } from './app_logic'; + +describe('AppLogic', () => { + beforeEach(() => { + resetContext({}); + AppLogic.mount(); + }); + + const DEFAULT_VALUES = { + hasInitialized: false, + }; + + it('has expected default values', () => { + expect(AppLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('initializeAppData()', () => { + it('sets values based on passed props', () => { + AppLogic.actions.initializeAppData(DEFAULT_INITIAL_APP_DATA); + + expect(AppLogic.values).toEqual({ + hasInitialized: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts new file mode 100644 index 00000000000000..0fb3bb8080d827 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -0,0 +1,31 @@ +/* + * 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 { kea } from 'kea'; + +import { IInitialAppData } from '../../../common/types'; +import { IKeaLogic } from '../shared/types'; + +export interface IAppLogicValues { + hasInitialized: boolean; +} +export interface IAppLogicActions { + initializeAppData(props: IInitialAppData): void; +} + +export const AppLogic = kea({ + actions: (): IAppLogicActions => ({ + initializeAppData: (props) => props, + }), + reducers: () => ({ + hasInitialized: [ + false, + { + initializeAppData: () => true, + }, + ], + }), +}) as IKeaLogic; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index fa9a761a966e15..0f4072c591bc7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -5,27 +5,68 @@ */ import '../__mocks__/shallow_usecontext.mock'; +import '../__mocks__/kea.mock'; import React, { useContext } from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; +import { useValues, useActions } from 'kea'; +import { SetupGuide } from './components/setup_guide'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { AppSearch, AppSearchNav } from './'; +import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; describe('AppSearch', () => { - it('renders', () => { + it('renders AppSearchUnconfigured when config.host is not set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); - expect(wrapper.find(Layout)).toHaveLength(1); + expect(wrapper.find(AppSearchUnconfigured)).toHaveLength(1); }); - it('redirects to Setup Guide when config.host is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + it('renders AppSearchConfigured when config.host set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); const wrapper = shallow(); + expect(wrapper.find(AppSearchConfigured)).toHaveLength(1); + }); +}); + +describe('AppSearchUnconfigured', () => { + it('renders the Setup Guide and redirects to the Setup Guide', () => { + const wrapper = shallow(); + + expect(wrapper.find(SetupGuide)).toHaveLength(1); expect(wrapper.find(Redirect)).toHaveLength(1); - expect(wrapper.find(Layout)).toHaveLength(0); + }); +}); + +describe('AppSearchConfigured', () => { + it('renders with layout', () => { + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData: () => {} })); + + const wrapper = shallow(); + + expect(wrapper.find(Layout)).toHaveLength(1); + }); + + it('initializes app data with passed props', () => { + const initializeAppData = jest.fn(); + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); + + shallow(); + + expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true }); + }); + + it('does not re-initialize app data', () => { + const initializeAppData = jest.fn(); + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ hasInitialized: true })); + + shallow(); + + expect(initializeAppData).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 5856a13bf75b71..5f4734630624c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; +import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; -import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { KibanaContext, IKibanaContext } from '../index'; +import { AppLogic, IAppLogicActions, IAppLogicValues } from './app_logic'; +import { IInitialAppData } from '../../../common/types'; + +import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; import { @@ -25,20 +29,29 @@ import { import { SetupGuide } from './components/setup_guide'; import { EngineOverview } from './components/engine_overview'; -export const AppSearch: React.FC = () => { +export const AppSearch: React.FC = (props) => { const { config } = useContext(KibanaContext) as IKibanaContext; + return !config.host ? : ; +}; + +export const AppSearchUnconfigured: React.FC = () => ( + + + + + + + + +); + +export const AppSearchConfigured: React.FC = (props) => { + const { hasInitialized } = useValues(AppLogic) as IAppLogicValues; + const { initializeAppData } = useActions(AppLogic) as IAppLogicActions; - if (!config.host) - return ( - - - - - - - - - ); + useEffect(() => { + if (!hasInitialized) initializeAppData(props); + }, [hasInitialized]); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 1b1f9ae43e7c1d..d6cc6e81509b27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -8,6 +8,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import { getContext, resetContext } from 'kea'; + import { I18nProvider } from '@kbn/i18n/react'; import { AppMountParameters, @@ -19,6 +23,7 @@ import { import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; import { LicenseProvider } from './shared/licensing'; import { IExternalUrl } from './shared/enterprise_search_url'; +import { IInitialAppData } from '../../common/types'; export interface IKibanaContext { config: { host?: string }; @@ -38,33 +43,41 @@ export const KibanaContext = React.createContext({}); */ export const renderApp = ( - App: React.FC, + App: React.FC, params: AppMountParameters, core: CoreStart, plugins: PluginsSetup, config: ClientConfigType, - data: ClientData + { externalUrl, ...initialData }: ClientData ) => { + resetContext({ createStore: true }); + const store = getContext().store as Store; + ReactDOM.render( - - - + + + + + , params.element ); - return () => ReactDOM.unmountComponentAtNode(params.element); + return () => { + resetContext({}); + ReactDOM.unmountComponentAtNode(params.element); + }; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts deleted file mode 100644 index 42f308c5542688..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getPublicUrl } from './'; - -describe('Enterprise Search URL helper', () => { - const httpMock = { get: jest.fn() } as any; - - it('calls and returns the public URL API endpoint', async () => { - httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://some.vanity.url' })); - - expect(await getPublicUrl(httpMock)).toEqual('http://some.vanity.url'); - }); - - it('strips trailing slashes', async () => { - httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://trailing.slash/' })); - - expect(await getPublicUrl(httpMock)).toEqual('http://trailing.slash'); - }); - - // For the most part, error logging/handling is done on the server side. - // On the front-end, we should simply gracefully fall back to config.host - // if we can't fetch a public URL - it('falls back to an empty string', async () => { - expect(await getPublicUrl(httpMock)).toEqual(''); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts deleted file mode 100644 index 419c187a0048a9..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { HttpSetup } from 'src/core/public'; - -/** - * On Elastic Cloud, the host URL set in kibana.yml is not necessarily the same - * URL we want to send users to in the front-end (e.g. if a vanity URL is set). - * - * This helper checks a Kibana API endpoint (which has checks an Enterprise - * Search internal API endpoint) for the correct public-facing URL to use. - */ -export const getPublicUrl = async (http: HttpSetup): Promise => { - try { - const { publicUrl } = await http.get('/api/enterprise_search/public_url'); - return stripTrailingSlash(publicUrl); - } catch { - return ''; - } -}; - -const stripTrailingSlash = (url: string): string => { - return url.endsWith('/') ? url.slice(0, -1) : url; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts index 563d19f9fdeb56..d2d82a43c6dd90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getPublicUrl } from './get_enterprise_search_url'; export { ExternalUrl, IExternalUrl } from './generate_external_url'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index 5969fa7806a446..72e4f2f0914967 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -13,6 +13,7 @@ import { EuiIcon, EuiTitle, EuiText, EuiLink as EuiLinkExternal } from '@elastic import { EuiLink } from '../react_router_helpers'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants'; +import { stripTrailingSlash } from '../../../../common/strip_trailing_slash'; import { NavContext, INavContext } from './layout'; @@ -78,7 +79,7 @@ export const SideNavLink: React.FC = ({ const { closeNavigation } = useContext(NavContext) as INavContext; const { pathname } = useLocation(); - const currentPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; + const currentPath = stripTrailingSlash(pathname); const isActive = currentPath === to || (isRoot && currentPath === ''); const classes = classNames('enterpriseSearchNavLinks__item', className, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts index 43cff5de6668da..395d2044e7dbce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts @@ -5,14 +5,13 @@ */ import { IOverviewValues } from '../overview_logic'; -import { IAccount, IOrganization, IUser } from '../../../types'; +import { IAccount, IOrganization } from '../../../types'; export const mockLogicValues = { accountsCount: 0, activityFeed: [], canCreateContentSources: false, canCreateInvitations: false, - currentUser: {} as IUser, fpAccount: {} as IAccount, hasOrgSources: false, hasUsers: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx index 3cf88cf120cc4c..acbc66259c2a10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx @@ -24,7 +24,6 @@ const account = { isAdmin: true, canCreatePersonalSources: true, groups: [], - supportEligible: true, isCurated: false, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts index 285ec9b9733785..7df4de4719f31e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts @@ -31,14 +31,13 @@ describe('OverviewLogic', () => { describe('setServerData', () => { const feed = [{ foo: 'bar' }] as any; - const user = { firstName: 'Joe', email: 'e@e.e', name: 'Joe Jo', color: 'pearl' }; const account = { - name: 'Jane doe', id: '1243', + groups: ['Default'], isAdmin: true, + isCurated: false, canCreatePersonalSources: true, - groups: [], - supportEligible: true, + viewedOnboardingPage: false, }; const org = { name: 'ACME', defaultOrgName: 'Org' }; @@ -47,7 +46,6 @@ describe('OverviewLogic', () => { activityFeed: feed, canCreateContentSources: true, canCreateInvitations: true, - currentUser: user, fpAccount: account, hasOrgSources: true, hasUsers: true, @@ -70,7 +68,6 @@ describe('OverviewLogic', () => { it('will set server values', () => { expect(OverviewLogic.values.organization).toEqual(org); expect(OverviewLogic.values.isFederatedAuth).toEqual(false); - expect(OverviewLogic.values.currentUser).toEqual(user); expect(OverviewLogic.values.fpAccount).toEqual(account); expect(OverviewLogic.values.canCreateInvitations).toEqual(true); expect(OverviewLogic.values.hasUsers).toEqual(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts index f1b4f447f74453..8bb177a2e742b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts @@ -8,7 +8,7 @@ import { HttpSetup } from 'src/core/public'; import { kea } from 'kea'; -import { IAccount, IOrganization, IUser } from '../../types'; +import { IAccount, IOrganization } from '../../types'; import { IFlashMessagesProps, IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; import { IFeedActivity } from './recent_activity'; @@ -26,7 +26,6 @@ export interface IOverviewServerData { activityFeed: IFeedActivity[]; organization: IOrganization; isFederatedAuth: boolean; - currentUser: IUser; fpAccount: IAccount; } @@ -63,12 +62,6 @@ export const OverviewLogic = kea({ setServerData: (_, { isFederatedAuth }) => isFederatedAuth, }, ], - currentUser: [ - {} as IUser, - { - setServerData: (_, { currentUser }) => currentUser, - }, - ], fpAccount: [ {} as IAccount, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 4aa171a5a57625..94462aa8de7d16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -6,14 +6,8 @@ import React, { useContext } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import { Store } from 'redux'; -import { getContext, resetContext } from 'kea'; - -resetContext({ createStore: true }); - -const store = getContext().store as Store; +import { IInitialAppData } from '../../../common/types'; import { KibanaContext, IKibanaContext } from '../index'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav } from './components/layout/nav'; @@ -23,7 +17,7 @@ import { SETUP_GUIDE_PATH } from './routes'; import { SetupGuide } from './components/setup_guide'; import { Overview } from './components/overview'; -export const WorkplaceSearch: React.FC = () => { +export const WorkplaceSearch: React.FC = (props) => { const { config } = useContext(KibanaContext) as IKibanaContext; if (!config.host) return ( @@ -38,25 +32,23 @@ export const WorkplaceSearch: React.FC = () => { ); return ( - - - - - - - - - - }> - - - {/* Will replace with groups component subsequent PR */} -
- - - - - - + + + + + + + + + }> + + + {/* Will replace with groups component subsequent PR */} +
+ + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 77c35adef3300a..a8348a6f69a394 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -4,24 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IAccount { - id: string; - isCurated?: boolean; - isAdmin: boolean; - canCreatePersonalSources: boolean; - groups: string[]; - supportEligible: boolean; -} - -export interface IOrganization { - name: string; - defaultOrgName: string; -} -export interface IUser { - firstName: string; - email: string; - name: string; - color: string; -} +export * from '../../../common/types/workplace_search'; export type TSpacerSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 0d392eefe0aa23..148a50fb4a5cee 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -20,19 +20,16 @@ import { import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { LicensingPluginSetup } from '../../licensing/public'; +import { IInitialAppData } from '../common/types'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../common/constants'; -import { - getPublicUrl, - ExternalUrl, - IExternalUrl, -} from './applications/shared/enterprise_search_url'; +import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url'; import AppSearchLogo from './applications/app_search/assets/logo.svg'; import WorkplaceSearchLogo from './applications/workplace_search/assets/logo.svg'; export interface ClientConfigType { host?: string; } -export interface ClientData { +export interface ClientData extends IInitialAppData { externalUrl: IExternalUrl; } @@ -119,10 +116,14 @@ export class EnterpriseSearchPlugin implements Plugin { if (!this.config.host) return; // No API to call if (this.hasInitialized) return; // We've already made an initial call - // TODO: Rename to something more generic once we start fetching more data than just external_url from this endpoint - const publicUrl = await getPublicUrl(http); + try { + const { publicUrl, ...initialData } = await http.get('/api/enterprise_search/config_data'); + this.data = { ...this.data, ...initialData }; + if (publicUrl) this.data.externalUrl = new ExternalUrl(publicUrl); - if (publicUrl) this.data.externalUrl = new ExternalUrl(publicUrl); - this.hasInitialized = true; + this.hasInitialized = true; + } catch { + // The plugin will attempt to re-fetch config data on page change + } } } diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index ee96f8099cf7ca..c26ada77f504fa 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -11,6 +11,7 @@ const { Response } = jest.requireActual('node-fetch'); import { loggingSystemMock } from 'src/core/server/mocks'; +import { DEFAULT_INITIAL_APP_DATA } from '../../common/__mocks__'; import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; describe('callEnterpriseSearchConfigAPI', () => { @@ -35,13 +36,50 @@ describe('callEnterpriseSearchConfigAPI', () => { }, settings: { external_url: 'http://some.vanity.url/', + read_only_mode: false, + ilm_enabled: true, + configured_limits: { + max_document_byte_size: 102400, + max_engines_per_meta_engine: 15, + }, + app_search: { + account_id: 'some-id-string', + onboarding_complete: true, + }, + workplace_search: { + organization: { + name: 'ACME Donuts', + default_org_name: 'My Organization', + }, + fp_account: { + id: 'some-id-string', + groups: ['Default', 'Cats'], + is_admin: true, + can_create_personal_sources: true, + is_curated: false, + viewed_onboarding_page: true, + }, + }, }, - access: { - user: 'someuser', - products: { + current_user: { + name: 'someuser', + access: { app_search: true, workplace_search: false, }, + app_search_role: { + id: 'account_id:somestring|user_oid:somestring', + role_type: 'owner', + ability: { + access_all_engines: true, + destroy: ['session'], + manage: ['account_credentials', 'account_engines'], // etc + edit: ['LocoMoco::Account'], // etc + view: ['Engine'], // etc + credential_types: ['admin', 'private', 'search'], + available_role_types: ['owner', 'admin'], + }, + }, }, }; @@ -56,11 +94,61 @@ describe('callEnterpriseSearchConfigAPI', () => { }); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ - publicUrl: 'http://some.vanity.url/', access: { hasAppSearchAccess: true, hasWorkplaceSearchAccess: false, }, + publicUrl: 'http://some.vanity.url', + ...DEFAULT_INITIAL_APP_DATA, + }); + }); + + it('falls back without error when data is unavailable', async () => { + fetchMock.mockImplementationOnce((url: string) => Promise.resolve(new Response('{}'))); + + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ + access: { + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }, + publicUrl: undefined, + readOnlyMode: false, + ilmEnabled: false, + configuredLimits: { + maxDocumentByteSize: undefined, + maxEnginesPerMetaEngine: undefined, + }, + appSearch: { + accountId: undefined, + onBoardingComplete: false, + role: { + id: undefined, + roleType: undefined, + ability: { + accessAllEngines: false, + destroy: [], + manage: [], + edit: [], + view: [], + credentialTypes: [], + availableRoleTypes: [], + }, + }, + }, + workplaceSearch: { + organization: { + name: undefined, + defaultOrgName: undefined, + }, + fpAccount: { + id: undefined, + groups: [], + isAdmin: false, + canCreatePersonalSources: false, + isCurated: false, + viewedOnboardingPage: false, + }, + }, }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 7a6d1eac1b4545..1dbec76806ba8e 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -11,14 +11,17 @@ import { KibanaRequest, Logger } from 'src/core/server'; import { ConfigType } from '../'; import { IAccess } from './check_access'; +import { IInitialAppData } from '../../common/types'; +import { stripTrailingSlash } from '../../common/strip_trailing_slash'; + interface IParams { request: KibanaRequest; config: ConfigType; log: Logger; } -interface IReturn { - publicUrl?: string; +interface IReturn extends IInitialAppData { access?: IAccess; + publicUrl?: string; } /** @@ -57,10 +60,50 @@ export const callEnterpriseSearchConfigAPI = async ({ const data = await response.json(); return { - publicUrl: data?.settings?.external_url, access: { - hasAppSearchAccess: !!data?.access?.products?.app_search, - hasWorkplaceSearchAccess: !!data?.access?.products?.workplace_search, + hasAppSearchAccess: !!data?.current_user?.access?.app_search, + hasWorkplaceSearchAccess: !!data?.current_user?.access?.workplace_search, + }, + publicUrl: stripTrailingSlash(data?.settings?.external_url), + readOnlyMode: !!data?.settings?.read_only_mode, + ilmEnabled: !!data?.settings?.ilm_enabled, + configuredLimits: { + maxDocumentByteSize: data?.settings?.configured_limits?.max_document_byte_size, + maxEnginesPerMetaEngine: data?.settings?.configured_limits?.max_engines_per_meta_engine, + }, + appSearch: { + accountId: data?.settings?.app_search?.account_id, + onBoardingComplete: !!data?.settings?.app_search?.onboarding_complete, + role: { + id: data?.current_user?.app_search_role?.id, + roleType: data?.current_user?.app_search_role?.role_type, + ability: { + accessAllEngines: !!data?.current_user?.app_search_role?.ability?.access_all_engines, + destroy: data?.current_user?.app_search_role?.ability?.destroy || [], + manage: data?.current_user?.app_search_role?.ability?.manage || [], + edit: data?.current_user?.app_search_role?.ability?.edit || [], + view: data?.current_user?.app_search_role?.ability?.view || [], + credentialTypes: data?.current_user?.app_search_role?.ability?.credential_types || [], + availableRoleTypes: + data?.current_user?.app_search_role?.ability?.available_role_types || [], + }, + }, + }, + workplaceSearch: { + organization: { + name: data?.settings?.workplace_search?.organization?.name, + defaultOrgName: data?.settings?.workplace_search?.organization?.default_org_name, + }, + fpAccount: { + id: data?.settings?.workplace_search?.fp_account.id, + groups: data?.settings?.workplace_search?.fp_account.groups || [], + isAdmin: !!data?.settings?.workplace_search?.fp_account?.is_admin, + canCreatePersonalSources: !!data?.settings?.workplace_search?.fp_account + ?.can_create_personal_sources, + isCurated: !!data?.settings?.workplace_search?.fp_account.is_curated, + viewedOnboardingPage: !!data?.settings?.workplace_search?.fp_account + .viewed_onboarding_page, + }, }, }; } catch (err) { diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 6de6671337797d..770ea8d420c20e 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -26,7 +26,7 @@ import { } from '../common/constants'; import { ConfigType } from './'; import { checkAccess } from './lib/check_access'; -import { registerPublicUrlRoute } from './routes/enterprise_search/public_url'; +import { registerConfigDataRoute } from './routes/enterprise_search/config_data'; import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; @@ -105,7 +105,7 @@ export class EnterpriseSearchPlugin implements Plugin { const router = http.createRouter(); const dependencies = { router, config, log: this.logger }; - registerPublicUrlRoute(dependencies); + registerConfigDataRoute(dependencies); registerEnginesRoute(dependencies); registerWSOverviewRoute(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts similarity index 50% rename from x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts rename to x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts index 846aae3fce56f0..7484e27594df4c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { MockRouter, mockDependencies } from '../__mocks__'; jest.mock('../../lib/enterprise_search_config_api', () => ({ @@ -11,41 +12,51 @@ jest.mock('../../lib/enterprise_search_config_api', () => ({ })); import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; -import { registerPublicUrlRoute } from './public_url'; +import { registerConfigDataRoute } from './config_data'; -describe('Enterprise Search Public URL API', () => { +describe('Enterprise Search Config Data API', () => { let mockRouter: MockRouter; beforeEach(() => { mockRouter = new MockRouter({ method: 'get' }); - registerPublicUrlRoute({ + registerConfigDataRoute({ ...mockDependencies, router: mockRouter.router, }); }); - describe('GET /api/enterprise_search/public_url', () => { - it('returns a publicUrl', async () => { + describe('GET /api/enterprise_search/config_data', () => { + it('returns an initial set of config data from Enterprise Search', async () => { + const mockData = { + access: { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }, + publicUrl: 'http://localhost:3002', + ...DEFAULT_INITIAL_APP_DATA, + }; + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ publicUrl: 'http://some.vanity.url' }); + return Promise.resolve(mockData); }); - await mockRouter.callRoute({}); expect(mockRouter.response.ok).toHaveBeenCalledWith({ - body: { publicUrl: 'http://some.vanity.url' }, + body: mockData, headers: { 'content-type': 'application/json' }, }); }); - // For the most part, all error logging is handled by callEnterpriseSearchConfigAPI. - // This endpoint should mostly just fall back gracefully to an empty string - it('falls back to an empty string', async () => { + it('throws a 502 error if data returns an empty obj', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve({}); + }); await mockRouter.callRoute({}); - expect(mockRouter.response.ok).toHaveBeenCalledWith({ - body: { publicUrl: '' }, - headers: { 'content-type': 'application/json' }, + + expect(mockRouter.response.customError).toHaveBeenCalledWith({ + statusCode: 502, + body: 'Error fetching data from Enterprise Search', }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts new file mode 100644 index 00000000000000..453c7fd99bf4c2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouteDependencies } from '../../plugin'; +import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; + +export function registerConfigDataRoute({ router, config, log }: IRouteDependencies) { + router.get( + { + path: '/api/enterprise_search/config_data', + validate: false, + }, + async (context, request, response) => { + const data = await callEnterpriseSearchConfigAPI({ request, config, log }); + + if (!Object.keys(data).length) { + return response.customError({ + statusCode: 502, + body: 'Error fetching data from Enterprise Search', + }); + } else { + return response.ok({ + body: data, + headers: { 'content-type': 'application/json' }, + }); + } + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts deleted file mode 100644 index a9edd4eb10da03..00000000000000 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IRouteDependencies } from '../../plugin'; -import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; - -export function registerPublicUrlRoute({ router, config, log }: IRouteDependencies) { - router.get( - { - path: '/api/enterprise_search/public_url', - validate: false, - }, - async (context, request, response) => { - const { publicUrl = '' } = - (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; - - return response.ok({ - body: { publicUrl }, - headers: { 'content-type': 'application/json' }, - }); - } - ); -} From 471b11408929cb58b3fc10f886bef220f93378d6 Mon Sep 17 00:00:00 2001 From: DeDe Morton Date: Fri, 21 Aug 2020 09:08:07 -0700 Subject: [PATCH 14/77] [DOCS] Update links to Beats documentation (#70380) * Update links to Beats documentation * Update snapshot files * Fix lint errors --- docs/management/watcher-ui/index.asciidoc | 2 +- .../monitoring/monitoring-metricbeat.asciidoc | 2 +- .../public/doc_links/doc_links_service.ts | 4 ++-- .../server/tutorials/cloudwatch_logs/index.ts | 3 ++- .../instructions/auditbeat_instructions.ts | 16 ++++++------- .../instructions/filebeat_instructions.ts | 16 ++++++------- .../instructions/functionbeat_instructions.ts | 17 ++++++++----- .../instructions/heartbeat_instructions.ts | 21 ++++++++-------- .../instructions/metricbeat_instructions.ts | 23 +++++++++++------- .../instructions/winlogbeat_instructions.ts | 5 ++-- .../server/tutorials/uptime_monitors/index.ts | 2 +- .../logs/__snapshots__/reason.test.js.snap | 4 ++-- .../public/components/logs/reason.js | 4 ++-- .../flyout/__snapshots__/flyout.test.js.snap | 24 +++++++++---------- .../apm/enable_metricbeat_instructions.js | 4 ++-- .../beats/enable_metricbeat_instructions.js | 4 ++-- .../enable_metricbeat_instructions.js | 2 +- .../kibana/enable_metricbeat_instructions.js | 2 +- .../enable_metricbeat_instructions.js | 2 +- 19 files changed, 86 insertions(+), 71 deletions(-) diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index fbe5fcd5cd3a5c..23a0acbff57182 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -60,7 +60,7 @@ The following example walks you through creating a threshold alert. The alert is triggered when the maximum total CPU usage on a machine goes above a certain percentage. The example uses https://www.elastic.co/products/beats/metricbeat[Metricbeat] to collect metrics from your systems and services. -{metricbeat-ref}/metricbeat-installation.html[Learn more] on how to install +{metricbeat-ref}/metricbeat-installation-configuration.html[Learn more] on how to install and get started with Metricbeat. [float] diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index d18ebe95c7974d..5ef3b8177a9c59 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -82,7 +82,7 @@ For more information, see {ref}/monitoring-settings.html[Monitoring settings in and {ref}/cluster-update-settings.html[Cluster update settings]. -- -. {metricbeat-ref}/metricbeat-installation.html[Install {metricbeat}] on the +. {metricbeat-ref}/metricbeat-installation-configuration.html[Install {metricbeat}] on the same server as {kib}. . Enable the {kib} {xpack} module in {metricbeat}. + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index bd279baa78d981..fc753517fd940e 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -41,8 +41,8 @@ export class DocLinksService { }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, - installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation.html`, - configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-configuration.html`, + installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation-configuration.html`, + configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/configuring-howto-filebeat.html`, elasticsearchOutput: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/elasticsearch-output.html`, startup: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-starting.html`, exportedFields: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/exported-fields.html`, diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index fb7b07c5dc1af2..6b017fae1e21f2 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -47,7 +47,8 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc an AWS Lambda function. \ [Learn more]({learnMoreLink}).', values: { - learnMoreLink: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html', + learnMoreLink: + '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', }, }), euiIconType: 'logoAWS', diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index 2a6cfa03587095..b6f7aa8c53ac91 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -31,9 +31,9 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Auditbeat', }), textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', }, }), commands: [ @@ -47,9 +47,9 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Auditbeat', }), textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', }, }), commands: [ @@ -68,9 +68,9 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Auditbeat', }), textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', }, }), commands: [ @@ -92,7 +92,7 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.auditbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Auditbeat? See the [Getting Started Guide]({guideLinkUrl}).\n\ + 'First time using Auditbeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Auditbeat Windows zip file from the [Download]({auditbeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Auditbeat`.\n\ @@ -101,7 +101,7 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ 5. From the PowerShell prompt, run the following commands to install Auditbeat as a Windows service.', values: { folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + guideLinkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', auditbeatLinkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', directoryName: 'auditbeat-{config.kibana.version}-windows', }, diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index 0e99033b2ea691..c760840165bfc9 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -31,9 +31,9 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Filebeat', }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', }, }), commands: [ @@ -47,9 +47,9 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Filebeat', }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', }, }), commands: [ @@ -68,9 +68,9 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Filebeat', }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', }, }), commands: [ @@ -90,7 +90,7 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Filebeat? See the [Getting Started Guide]({guideLinkUrl}).\n\ + 'First time using Filebeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Filebeat Windows zip file from the [Download]({filebeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Filebeat`.\n\ @@ -99,7 +99,7 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ 5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', values: { folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', directoryName: 'filebeat-{config.kibana.version}-windows', }, diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index 06ff84146b5d88..61e76bd9d3c185 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -31,8 +31,10 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Functionbeat', }), textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Functionbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html' }, + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -47,8 +49,10 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ textPre: i18n.translate( 'home.tutorials.common.functionbeatInstructions.install.linuxTextPre', { - defaultMessage: 'First time using Functionbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html' }, + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, } ), commands: [ @@ -65,7 +69,7 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.functionbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Functionbeat? See the [Getting Started Guide]({functionbeatLink}).\n\ + 'First time using Functionbeat? See the [Quick Start]({functionbeatLink}).\n\ 1. Download the Functionbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Functionbeat`.\n\ @@ -75,7 +79,8 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`functionbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - functionbeatLink: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html', + functionbeatLink: + '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/functionbeat', }, } diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index fa5bf5df13b6ba..4d519ad8aa01ee 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -31,8 +31,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Heartbeat', }), textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html' }, + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -45,8 +45,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Heartbeat', }), textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html' }, + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-amd64.deb', @@ -62,8 +62,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Heartbeat', }), textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html' }, + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-x86_64.rpm', @@ -82,7 +82,7 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.heartbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Heartbeat? See the [Getting Started Guide]({heartbeatLink}).\n\ + 'First time using Heartbeat? See the [Quick Start]({heartbeatLink}).\n\ 1. Download the Heartbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Heartbeat`.\n\ @@ -92,7 +92,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`heartbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - heartbeatLink: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html', + heartbeatLink: + '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/heartbeat', }, } @@ -357,7 +358,7 @@ export function heartbeatEnableInstructionsOnPrem() { 'Where {hostTemplate} is your monitored URL, For more details on how to configure Monitors in \ Heartbeat, read the [Heartbeat configuration docs.]({configureLink})', values: { - configureLink: '{config.docs.beats.heartbeat}/heartbeat-configuration.html', + configureLink: '{config.docs.beats.heartbeat}/configuring-howto-heartbeat.html', hostTemplate: '``', }, } @@ -428,7 +429,7 @@ export function heartbeatEnableInstructionsCloud() { { defaultMessage: 'For more details on how to configure Monitors in Heartbeat, read the [Heartbeat configuration docs.]({configureLink})', - values: { configureLink: '{config.docs.beats.heartbeat}/heartbeat-configuration.html' }, + values: { configureLink: '{config.docs.beats.heartbeat}/configuring-howto-heartbeat.html' }, } ); return { diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index 651405941610f1..cce93e0dfb527f 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -31,8 +31,10 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Metricbeat', }), textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -45,8 +47,10 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Metricbeat', }), textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', @@ -62,8 +66,10 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Metricbeat', }), textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', @@ -82,7 +88,7 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.metricbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Metricbeat? See the [Getting Started Guide]({metricbeatLink}).\n\ + 'First time using Metricbeat? See the [Quick Start]({metricbeatLink}).\n\ 1. Download the Metricbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Metricbeat`.\n\ @@ -92,7 +98,8 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`metricbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - metricbeatLink: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html', + metricbeatLink: + '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', }, } diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index 27d7822e080a32..1eacbb729aee40 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -34,7 +34,7 @@ export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Winlogbeat? See the [Getting Started Guide]({winlogbeatLink}).\n\ + 'First time using Winlogbeat? See the [Quick Start]({winlogbeatLink}).\n\ 1. Download the Winlogbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Winlogbeat`.\n\ @@ -44,7 +44,8 @@ export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`winlogbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - winlogbeatLink: '{config.docs.beats.winlogbeat}/winlogbeat-getting-started.html', + winlogbeatLink: + '{config.docs.beats.winlogbeat}/winlogbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/winlogbeat', }, } diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index 7366583e597781..96b81c9fb41812 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -47,7 +47,7 @@ export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSc Given a list of URLs, Heartbeat asks the simple question: Are you alive? \ [Learn more]({learnMoreLink}).', values: { - learnMoreLink: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html', + learnMoreLink: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', }, }), euiIconType: 'uptimeApp', diff --git a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap index b63fe7047e96c9..c925ecd1c98ff8 100644 --- a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap @@ -13,7 +13,7 @@ exports[`Logs should render a default message 1`] = ` values={ Object { "link": Filebeat diff --git a/x-pack/plugins/monitoring/public/components/logs/reason.js b/x-pack/plugins/monitoring/public/components/logs/reason.js index ad21f7f81d9bdb..55dca72bf645d3 100644 --- a/x-pack/plugins/monitoring/public/components/logs/reason.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.js @@ -24,7 +24,7 @@ export const Reason = ({ reason }) => { link: ( { link: ( {i18n.translate('xpack.monitoring.logs.reason.noIndexPatternLink', { defaultMessage: 'Filebeat', diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap index c5507efb989dec..2f29cd9122a610 100644 --- a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap @@ -156,7 +156,7 @@ exports[`Flyout apm part two should show instructions to migrate to metricbeat 1 "children":

Date: Fri, 21 Aug 2020 18:08:25 +0200 Subject: [PATCH 15/77] [Lens] Register saved object references (#74523) --- x-pack/plugins/lens/common/types.ts | 10 + .../lens/public/app_plugin/app.test.tsx | 137 +++++++---- x-pack/plugins/lens/public/app_plugin/app.tsx | 92 ++++--- .../visualization.test.tsx | 19 +- .../datatable_visualization/visualization.tsx | 11 +- .../__mocks__/expression_helpers.ts | 14 ++ .../editor_frame/config_panel/layer_panel.tsx | 1 - .../editor_frame/editor_frame.test.tsx | 109 +++------ .../editor_frame/editor_frame.tsx | 116 ++++----- .../editor_frame/expression_helpers.ts | 60 +---- .../editor_frame/save.test.ts | 37 ++- .../editor_frame_service/editor_frame/save.ts | 73 +++--- .../editor_frame/state_helpers.ts | 87 +++++++ .../editor_frame/state_management.test.ts | 8 +- .../editor_frame/suggestion_helpers.ts | 2 +- .../editor_frame/suggestion_panel.test.tsx | 1 - .../editor_frame/suggestion_panel.tsx | 41 ++-- .../workspace_panel/chart_switch.tsx | 2 +- .../workspace_panel/workspace_panel.test.tsx | 19 +- .../workspace_panel/workspace_panel.tsx | 21 +- .../embeddable/embeddable.test.tsx | 56 ++++- .../embeddable/embeddable.tsx | 60 ++++- .../embeddable/embeddable_factory.ts | 15 +- .../embeddable/expression_wrapper.tsx | 13 +- .../public/editor_frame_service/mocks.tsx | 4 +- .../public/editor_frame_service/service.tsx | 20 +- .../__mocks__/loader.ts | 4 + .../indexpattern.test.ts | 107 ++++---- .../indexpattern_datasource/indexpattern.tsx | 34 ++- .../indexpattern_datasource/loader.test.ts | 86 ++++++- .../public/indexpattern_datasource/loader.ts | 54 ++++- .../public/indexpattern_datasource/types.ts | 9 +- .../metric_visualization.test.ts | 9 +- .../metric_visualization.tsx | 16 +- .../lens/public/metric_visualization/types.ts | 2 - .../persistence/filter_references.test.ts | 99 ++++++++ .../public/persistence/filter_references.ts | 56 +++++ .../plugins/lens/public/persistence/index.ts | 1 + .../persistence/saved_object_store.test.ts | 51 ++-- .../public/persistence/saved_object_store.ts | 37 +-- .../pie_visualization/pie_visualization.tsx | 4 +- .../public/pie_visualization/to_expression.ts | 20 +- x-pack/plugins/lens/public/types.ts | 37 ++- .../xy_visualization/to_expression.test.ts | 16 +- .../public/xy_visualization/to_expression.ts | 32 +-- .../lens/public/xy_visualization/types.ts | 1 - .../xy_visualization/xy_visualization.test.ts | 6 - .../xy_visualization/xy_visualization.tsx | 6 +- .../__snapshots__/migrations.test.ts.snap | 188 ++++++++++++++ x-pack/plugins/lens/server/migrations.test.ts | 229 ++++++++++++++++++ x-pack/plugins/lens/server/migrations.ts | 136 ++++++++++- 51 files changed, 1609 insertions(+), 659 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts create mode 100644 x-pack/plugins/lens/public/persistence/filter_references.test.ts create mode 100644 x-pack/plugins/lens/public/persistence/filter_references.ts create mode 100644 x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 56a56bdc2d59c8..c572b59899fceb 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FilterMeta, Filter } from 'src/plugins/data/common'; + export interface ExistingFields { indexPatternTitle: string; existingFieldNames: string[]; @@ -13,3 +15,11 @@ export interface DateRange { fromDate: string; toDate: string; } + +export interface PersistableFilterMeta extends FilterMeta { + indexRefName?: string; +} + +export interface PersistableFilter extends Filter { + meta: PersistableFilterMeta; +} diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 70136a486e8c1a..442f82161512f1 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -33,7 +33,7 @@ import { navigationPluginMock } from '../../../../../src/plugins/navigation/publ import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { coreMock } from 'src/core/public/mocks'; -jest.mock('../persistence'); +jest.mock('../editor_frame_service/editor_frame/expression_helpers'); jest.mock('src/core/public'); jest.mock('../../../../../src/plugins/saved_objects/public', () => { // eslint-disable-next-line no-shadow @@ -284,11 +284,11 @@ describe('Lens App', () => { (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', title: 'Daaaaaaadaumching!', - expression: 'valid expression', state: { query: 'fake query', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, + references: [], }); await act(async () => { instance.setProps({ docId: '1234' }); @@ -346,12 +346,11 @@ describe('Lens App', () => { args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', - expression: 'valid expression', state: { query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], }); instance = mount(); @@ -375,15 +374,13 @@ describe('Lens App', () => { expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - doc: { + doc: expect.objectContaining({ id: '1234', - expression: 'valid expression', - state: { + state: expect.objectContaining({ query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, - }, - }, + }), + }), }) ); }); @@ -444,7 +441,6 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, } as jest.ResolvedValue); }); @@ -467,7 +463,12 @@ describe('Lens App', () => { } async function save({ - lastKnownDoc = { expression: 'kibana 3' }, + lastKnownDoc = { + references: [], + state: { + filters: [], + }, + }, initialDocId, ...saveProps }: SaveProps & { @@ -481,16 +482,14 @@ describe('Lens App', () => { args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', - expression: 'kibana', + references: [], state: { query: 'fake query', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, filters: [], }, }); (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ id: id || 'aaa', - expression: 'kibana 2', })); await act(async () => { @@ -508,6 +507,7 @@ describe('Lens App', () => { onChange({ filterableIndexPatterns: [], doc: { id: initialDocId, ...lastKnownDoc } as Document, + isSaveable: true, }) ); @@ -541,7 +541,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + doc: ({ id: 'will save this' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -560,7 +561,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + doc: ({ id: 'will save this' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -575,11 +577,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - id: undefined, - title: 'hello there', - expression: 'kibana 3', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: undefined, + title: 'hello there', + }) + ); expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); @@ -595,11 +598,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - id: undefined, - title: 'hello there', - expression: 'kibana 3', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: undefined, + title: 'hello there', + }) + ); expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); @@ -615,11 +619,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - id: '1234', - title: 'hello there', - expression: 'kibana 3', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: '1234', + title: 'hello there', + }) + ); expect(args.redirectTo).not.toHaveBeenCalled(); @@ -639,7 +644,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }) ); @@ -663,11 +669,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - expression: 'kibana 3', - id: undefined, - title: 'hello there', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: undefined, + title: 'hello there', + }) + ); expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, true); }); @@ -717,7 +724,8 @@ describe('Lens App', () => { await act(async () => onChange({ filterableIndexPatterns: [], - doc: ({ id: '123', expression: 'valid expression' } as unknown) as Document, + doc: ({ id: '123' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -756,7 +764,8 @@ describe('Lens App', () => { await act(async () => onChange({ filterableIndexPatterns: [], - doc: ({ expression: 'valid expression' } as unknown) as Document, + doc: ({} as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -779,7 +788,6 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, } as jest.ResolvedValue); }); @@ -824,8 +832,9 @@ describe('Lens App', () => { await act(async () => { onChange({ - filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + filterableIndexPatterns: ['1'], + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }); }); @@ -842,8 +851,9 @@ describe('Lens App', () => { await act(async () => { onChange({ - filterableIndexPatterns: [{ id: '2', title: 'second index' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + filterableIndexPatterns: ['2'], + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }); }); @@ -1078,11 +1088,11 @@ describe('Lens App', () => { (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', title: 'My cool doc', - expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, + references: [], } as jest.ResolvedValue); }); @@ -1114,7 +1124,12 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + doc: ({ + id: undefined, + + references: [], + } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1135,7 +1150,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + doc: ({ id: undefined, state: {} } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1159,7 +1175,12 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'different expression' } as unknown) as Document, + doc: ({ + id: '1234', + + references: [], + } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1183,7 +1204,16 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, + doc: ({ + id: '1234', + title: 'My cool doc', + references: [], + state: { + query: 'kuery', + filters: [], + }, + } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1207,7 +1237,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: null } as unknown) as Document, + doc: ({ id: '1234', references: [] } as unknown) as Document, + isSaveable: true, }) ); instance.update(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 5ca6f27a0c578d..021ca8b182b2b5 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -28,7 +28,7 @@ import { OnSaveProps, checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; -import { Document, SavedObjectStore } from '../persistence'; +import { Document, SavedObjectStore, injectFilterReferences } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -57,6 +57,7 @@ interface State { query: Query; filters: Filter[]; savedQuery?: SavedQuery; + isSaveable: boolean; } export function App({ @@ -100,6 +101,7 @@ export function App({ originatingApp, filters: data.query.filterManager.getFilters(), indicateNoData: false, + isSaveable: false, }; }); @@ -122,11 +124,7 @@ export function App({ const { lastKnownDoc } = state; - const isSaveable = - lastKnownDoc && - lastKnownDoc.expression && - lastKnownDoc.expression.length > 0 && - core.application.capabilities.visualize.save; + const savingPermitted = state.isSaveable && core.application.capabilities.visualize.save; useEffect(() => { // Clear app-specific filters when navigating to Lens. Necessary because Lens @@ -177,15 +175,34 @@ export function App({ history, ]); + const getLastKnownDocWithoutPinnedFilters = useCallback( + function () { + if (!lastKnownDoc) return undefined; + const [pinnedFilters, appFilters] = _.partition( + injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), + esFilters.isFilterPinned + ); + return pinnedFilters?.length + ? { + ...lastKnownDoc, + state: { + ...lastKnownDoc.state, + filters: appFilters, + }, + } + : lastKnownDoc; + }, + [lastKnownDoc] + ); + useEffect(() => { onAppLeave((actions) => { // Confirm when the user has made any changes to an existing doc // or when the user has configured something without saving if ( core.application.capabilities.visualize.save && - (state.persistedDoc?.expression - ? !_.isEqual(lastKnownDoc?.expression, state.persistedDoc.expression) - : lastKnownDoc?.expression) + !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) && + (state.isSaveable || state.persistedDoc) ) { return actions.confirm( i18n.translate('xpack.lens.app.unsavedWorkMessage', { @@ -199,7 +216,14 @@ export function App({ return actions.default(); } }); - }, [lastKnownDoc, onAppLeave, state.persistedDoc, core.application.capabilities.visualize.save]); + }, [ + lastKnownDoc, + onAppLeave, + state.persistedDoc, + state.isSaveable, + core.application.capabilities.visualize.save, + getLastKnownDocWithoutPinnedFilters, + ]); // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { @@ -248,13 +272,17 @@ export function App({ .load(docId) .then((doc) => { getAllIndexPatterns( - doc.state.datasourceMetaData.filterableIndexPatterns, + _.uniq( + doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ), data.indexPatterns, core.notifications ) .then((indexPatterns) => { // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters(doc.state.filters); + data.query.filterManager.setAppFilters( + injectFilterReferences(doc.state.filters, doc.references) + ); setState((s) => ({ ...s, isLoading: false, @@ -264,13 +292,13 @@ export function App({ indexPatternsForTopNav: indexPatterns, })); }) - .catch(() => { + .catch((e) => { setState((s) => ({ ...s, isLoading: false })); redirectTo(); }); }) - .catch(() => { + .catch((e) => { setState((s) => ({ ...s, isLoading: false })); core.notifications.toasts.addDanger( @@ -306,22 +334,9 @@ export function App({ if (!lastKnownDoc) { return; } - const [pinnedFilters, appFilters] = _.partition( - lastKnownDoc.state?.filters, - esFilters.isFilterPinned - ); - const lastDocWithoutPinned = pinnedFilters?.length - ? { - ...lastKnownDoc, - state: { - ...lastKnownDoc.state, - filters: appFilters, - }, - } - : lastKnownDoc; const doc = { - ...lastDocWithoutPinned, + ...getLastKnownDocWithoutPinnedFilters()!, description: saveProps.newDescription, id: saveProps.newCopyOnSave ? undefined : lastKnownDoc.id, title: saveProps.newTitle, @@ -411,7 +426,7 @@ export function App({ emphasize: true, iconType: 'check', run: () => { - if (isSaveable && lastKnownDoc) { + if (savingPermitted) { runSave({ newTitle: lastKnownDoc.title, newCopyOnSave: false, @@ -421,7 +436,7 @@ export function App({ } }, testId: 'lnsApp_saveAndReturnButton', - disableButton: !isSaveable, + disableButton: !savingPermitted, }, ] : []), @@ -436,12 +451,12 @@ export function App({ }), emphasize: !state.originatingApp || !lastKnownDoc?.id, run: () => { - if (isSaveable && lastKnownDoc) { + if (savingPermitted) { setState((s) => ({ ...s, isSaveModalVisible: true })); } }, testId: 'lnsApp_saveButton', - disableButton: !isSaveable, + disableButton: !savingPermitted, }, ]} data-test-subj="lnsApp_topNav" @@ -522,7 +537,10 @@ export function App({ doc: state.persistedDoc, onError, showNoDataPopover, - onChange: ({ filterableIndexPatterns, doc }) => { + onChange: ({ filterableIndexPatterns, doc, isSaveable }) => { + if (isSaveable !== state.isSaveable) { + setState((s) => ({ ...s, isSaveable })); + } if (!_.isEqual(state.persistedDoc, doc)) { setState((s) => ({ ...s, lastKnownDoc: doc })); } @@ -530,8 +548,8 @@ export function App({ // Update the cached index patterns if the user made a change to any of them if ( state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.find( - ({ id }) => + filterableIndexPatterns.some( + (id) => !state.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) ) ) { @@ -573,12 +591,12 @@ export function App({ } export async function getAllIndexPatterns( - ids: Array<{ id: string }>, + ids: string[], indexPatternsService: IndexPatternsContract, notifications: NotificationsStart ): Promise { try { - return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id))); + return await Promise.all(ids.map((id) => indexPatternsService.get(id))); } catch (e) { notifications.toasts.addDanger( i18n.translate('xpack.lens.app.indexPatternLoadingError', { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 0b6584277ffa77..194f12cf9291b6 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -50,20 +50,6 @@ describe('Datatable Visualization', () => { }); }); - describe('#getPersistableState', () => { - it('should persist the internal state', () => { - const expectedState: DatatableVisualizationState = { - layers: [ - { - layerId: 'baz', - columns: ['a', 'b', 'c'], - }, - ], - }; - expect(datatableVisualization.getPersistableState(expectedState)).toEqual(expectedState); - }); - }); - describe('#getLayerIds', () => { it('return the layer ids', () => { const state: DatatableVisualizationState = { @@ -340,7 +326,10 @@ describe('Datatable Visualization', () => { label: 'label', }); - const expression = datatableVisualization.toExpression({ layers: [layer] }, frame) as Ast; + const expression = datatableVisualization.toExpression( + { layers: [layer] }, + frame.datasourceLayers + ) as Ast; const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns'); expect(tableArgs).toHaveLength(1); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 659f8ea12bcb04..5aff4e14b17f2b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -25,10 +25,7 @@ function newLayerState(layerId: string): LayerState { }; } -export const datatableVisualization: Visualization< - DatatableVisualizationState, - DatatableVisualizationState -> = { +export const datatableVisualization: Visualization = { id: 'lnsDatatable', visualizationTypes: [ @@ -75,8 +72,6 @@ export const datatableVisualization: Visualization< ); }, - getPersistableState: (state) => state, - getSuggestions({ table, state, @@ -186,9 +181,9 @@ export const datatableVisualization: Visualization< }; }, - toExpression(state, frame): Ast { + toExpression(state, datasourceLayers): Ast { const layer = state.layers[0]; - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); // When we add a column it could be empty, and therefore have no order const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts new file mode 100644 index 00000000000000..e0b3616315cbd0 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.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 { Ast } from '@kbn/interpreter/common'; + +export function buildExpression(): Ast { + return { + type: 'expression', + chain: [{ type: 'function', function: 'test', arguments: {} }], + }; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 38224bf962a3f9..b2804cfddba58c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -124,7 +124,6 @@ export function LayerPanel( const nextPublicAPI = layerDatasource.getPublicAPI({ state: newState, layerId, - dateRange: props.framePublicAPI.dateRange, }); const nextTable = new Set( nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 2f7a78197b2b26..e628ea0675a8d0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -170,25 +170,22 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: datasource1State, testDatasource2: datasource2State, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); }); - expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State); - expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State); + expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, []); + expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, []); expect(mockDatasource3.initialize).not.toHaveBeenCalled(); }); @@ -425,21 +422,6 @@ describe('editor_frame', () => { "function": "kibana", "type": "function", }, - Object { - "arguments": Object { - "filters": Array [ - "[]", - ], - "query": Array [ - "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", - ], - "timeRange": Array [ - "{\\"from\\":\\"\\",\\"to\\":\\"\\"}", - ], - }, - "function": "kibana_context", - "type": "function", - }, Object { "arguments": Object { "layerIds": Array [ @@ -499,19 +481,16 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: {}, testDatasource2: {}, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -535,21 +514,6 @@ describe('editor_frame', () => { "function": "kibana", "type": "function", }, - Object { - "arguments": Object { - "filters": Array [ - "[]", - ], - "query": Array [ - "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", - ], - "timeRange": Array [ - "{\\"from\\":\\"\\",\\"to\\":\\"\\"}", - ], - }, - "function": "kibana_context", - "type": "function", - }, Object { "arguments": Object { "layerIds": Array [ @@ -747,19 +711,16 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: {}, testDatasource2: {}, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -802,19 +763,16 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: datasource1State, testDatasource2: datasource2State, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -842,7 +800,6 @@ describe('editor_frame', () => { it('should give access to the datasource state in the datasource factory function', async () => { const datasourceState = {}; - const dateRange = { fromDate: 'now-1w', toDate: 'now' }; mockDatasource.initialize.mockResolvedValue(datasourceState); mockDatasource.getLayers.mockReturnValue(['first']); @@ -850,7 +807,6 @@ describe('editor_frame', () => { mount( { }); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ - dateRange, state: datasourceState, layerId: 'first', }); @@ -1460,9 +1415,10 @@ describe('editor_frame', () => { }) ); mockDatasource.getLayers.mockReturnValue(['first']); - mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], - }); + mockDatasource.getPersistableState = jest.fn((x) => ({ + state: x, + savedObjectReferences: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + })); mockVisualization.initialize.mockReturnValue({ initialState: true }); await act(async () => { @@ -1487,14 +1443,20 @@ describe('editor_frame', () => { expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenNthCalledWith(1, { - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], + filterableIndexPatterns: ['1'], doc: { - expression: '', id: undefined, + description: undefined, + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], state: { visualization: null, // Not yet loaded - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'resolved' }] }, - datasourceStates: { testDatasource: undefined }, + datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1502,18 +1464,23 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: false, }); expect(onChange).toHaveBeenLastCalledWith({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], + filterableIndexPatterns: ['1'], doc: { - expression: '', + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], + description: undefined, id: undefined, state: { visualization: { initialState: true }, // Now loaded - datasourceMetaData: { - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], - }, - datasourceStates: { testDatasource: undefined }, + datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1521,6 +1488,7 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: false, }); }); @@ -1562,11 +1530,10 @@ describe('editor_frame', () => { expect(onChange).toHaveBeenNthCalledWith(3, { filterableIndexPatterns: [], doc: { - expression: expect.stringContaining('vis "expression"'), id: undefined, + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, - datasourceStates: { testDatasource: undefined }, + datasourceStates: { testDatasource: { datasource: '' } }, visualization: { initialState: true }, query: { query: 'new query', language: 'lucene' }, filters: [], @@ -1575,6 +1542,7 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: true, }); }); @@ -1583,9 +1551,10 @@ describe('editor_frame', () => { mockDatasource.initialize.mockResolvedValue({}); mockDatasource.getLayers.mockReturnValue(['first']); - mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], - }); + mockDatasource.getPersistableState = jest.fn((x) => ({ + state: x, + savedObjectReferences: [{ type: 'index-pattern', id: '1', name: '' }], + })); mockVisualization.initialize.mockReturnValue({ initialState: true }); await act(async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 48a3511a8f3594..72ad8e074226cc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -7,13 +7,7 @@ import React, { useEffect, useReducer } from 'react'; import { CoreSetup, CoreStart } from 'kibana/public'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; -import { - Datasource, - DatasourcePublicAPI, - FramePublicAPI, - Visualization, - DatasourceMetaData, -} from '../../types'; +import { Datasource, FramePublicAPI, Visualization } from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; import { ConfigPanelWrapper } from './config_panel'; @@ -26,6 +20,7 @@ import { getSavedObjectFormat } from './save'; import { generateId } from '../../id_generator'; import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; import { EditorFrameStartPlugins } from '../service'; +import { initializeDatasources, createDatasourceLayers } from './state_helpers'; export interface EditorFrameProps { doc?: Document; @@ -45,8 +40,9 @@ export interface EditorFrameProps { filters: Filter[]; savedQuery?: SavedQuery; onChange: (arg: { - filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + filterableIndexPatterns: string[]; doc: Document; + isSaveable: boolean; }) => void; showNoDataPopover: () => void; } @@ -67,25 +63,19 @@ export function EditorFrame(props: EditorFrameProps) { // prevents executing dispatch on unmounted component let isUnmounted = false; if (!allLoaded) { - Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => { - if ( - state.datasourceStates[datasourceId] && - state.datasourceStates[datasourceId].isLoading - ) { - datasource - .initialize(state.datasourceStates[datasourceId].state || undefined) - .then((datasourceState) => { - if (!isUnmounted) { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: datasourceState, - datasourceId, - }); - } - }) - .catch(onError); - } - }); + initializeDatasources(props.datasourceMap, state.datasourceStates, props.doc?.references) + .then((result) => { + if (!isUnmounted) { + Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: datasourceState, + datasourceId, + }); + }); + } + }) + .catch(onError); } return () => { isUnmounted = true; @@ -95,22 +85,7 @@ export function EditorFrame(props: EditorFrameProps) { [allLoaded, onError] ); - const datasourceLayers: Record = {}; - Object.keys(props.datasourceMap) - .filter((id) => state.datasourceStates[id] && !state.datasourceStates[id].isLoading) - .forEach((id) => { - const datasourceState = state.datasourceStates[id].state; - const datasource = props.datasourceMap[id]; - - const layers = datasource.getLayers(datasourceState); - layers.forEach((layer) => { - datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({ - state: datasourceState, - layerId: layer, - dateRange: props.dateRange, - }); - }); - }); + const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates); const framePublicAPI: FramePublicAPI = { datasourceLayers, @@ -165,7 +140,18 @@ export function EditorFrame(props: EditorFrameProps) { if (props.doc) { dispatch({ type: 'VISUALIZATION_LOADED', - doc: props.doc, + doc: { + ...props.doc, + state: { + ...props.doc.state, + visualization: props.doc.visualizationType + ? props.visualizationMap[props.doc.visualizationType].initialize( + framePublicAPI, + props.doc.state.visualization + ) + : props.doc.state.visualization, + }, + }, }); } else { dispatch({ @@ -206,36 +192,20 @@ export function EditorFrame(props: EditorFrameProps) { return; } - const indexPatterns: DatasourceMetaData['filterableIndexPatterns'] = []; - Object.entries(props.datasourceMap) - .filter(([id, datasource]) => { - const stateWrapper = state.datasourceStates[id]; - return ( - stateWrapper && - !stateWrapper.isLoading && - datasource.getLayers(stateWrapper.state).length > 0 - ); + props.onChange( + getSavedObjectFormat({ + activeDatasources: Object.keys(state.datasourceStates).reduce( + (datasourceMap, datasourceId) => ({ + ...datasourceMap, + [datasourceId]: props.datasourceMap[datasourceId], + }), + {} + ), + visualization: activeVisualization, + state, + framePublicAPI, }) - .forEach(([id, datasource]) => { - indexPatterns.push( - ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns - ); - }); - - const doc = getSavedObjectFormat({ - activeDatasources: Object.keys(state.datasourceStates).reduce( - (datasourceMap, datasourceId) => ({ - ...datasourceMap, - [datasourceId]: props.datasourceMap[datasourceId], - }), - {} - ), - visualization: activeVisualization, - state, - framePublicAPI, - }); - - props.onChange({ filterableIndexPatterns: indexPatterns, doc }); + ); }, // eslint-disable-next-line react-hooks/exhaustive-deps [ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index ee28ccfe1bf53b..952718e13c8cfd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -5,8 +5,7 @@ */ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { Visualization, Datasource, FramePublicAPI } from '../../types'; -import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public'; +import { Visualization, Datasource, DatasourcePublicAPI } from '../../types'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, @@ -58,40 +57,12 @@ export function prependDatasourceExpression( ? fromExpression(visualizationExpression) : visualizationExpression; - return { - type: 'expression', - chain: [datafetchExpression, ...parsedVisualizationExpression.chain], - }; -} - -export function prependKibanaContext( - expression: Ast | string, - { - timeRange, - query, - filters, - }: { - timeRange?: TimeRange; - query?: Query; - filters?: Filter[]; - } -): Ast { - const parsedExpression = typeof expression === 'string' ? fromExpression(expression) : expression; - return { type: 'expression', chain: [ { type: 'function', function: 'kibana', arguments: {} }, - { - type: 'function', - function: 'kibana_context', - arguments: { - timeRange: timeRange ? [JSON.stringify(timeRange)] : [], - query: query ? [JSON.stringify(query)] : [], - filters: [JSON.stringify(filters || [])], - }, - }, - ...parsedExpression.chain, + datafetchExpression, + ...parsedVisualizationExpression.chain, ], }; } @@ -101,8 +72,7 @@ export function buildExpression({ visualizationState, datasourceMap, datasourceStates, - framePublicAPI, - removeDateRange, + datasourceLayers, }: { visualization: Visualization | null; visualizationState: unknown; @@ -114,24 +84,12 @@ export function buildExpression({ state: unknown; } >; - framePublicAPI: FramePublicAPI; - removeDateRange?: boolean; + datasourceLayers: Record; }): Ast | null { if (visualization === null) { return null; } - const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI); - - const expressionContext = removeDateRange - ? { query: framePublicAPI.query, filters: framePublicAPI.filters } - : { - query: framePublicAPI.query, - timeRange: { - from: framePublicAPI.dateRange.fromDate, - to: framePublicAPI.dateRange.toDate, - }, - filters: framePublicAPI.filters, - }; + const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers); const completeExpression = prependDatasourceExpression( visualizationExpression, @@ -139,9 +97,5 @@ export function buildExpression({ datasourceStates ); - if (completeExpression) { - return prependKibanaContext(completeExpression, expressionContext); - } else { - return null; - } + return completeExpression; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index d72e5c57ce56e9..45d24fd30e2fcd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -8,14 +8,18 @@ import { getSavedObjectFormat, Props } from './save'; import { createMockDatasource, createMockVisualization } from '../mocks'; import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public'; +jest.mock('./expression_helpers'); + describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); - mockVisualization.getPersistableState.mockImplementation((x) => x); const mockDatasource = createMockDatasource('a'); const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern; const mockField = ({ name: '@timestamp' } as unknown) as IFieldType; - mockDatasource.getPersistableState.mockImplementation((x) => x); + mockDatasource.getPersistableState.mockImplementation((x) => ({ + state: x, + savedObjectReferences: [], + })); const saveArgs: Props = { activeDatasources: { indexpattern: mockDatasource, @@ -47,15 +51,17 @@ describe('save editor frame state', () => { it('transforms from internal state to persisted doc format', async () => { const datasource = createMockDatasource('a'); datasource.getPersistableState.mockImplementation((state) => ({ - stuff: `${state}_datasource_persisted`, + state: { + stuff: `${state}_datasource_persisted`, + }, + savedObjectReferences: [], })); + datasource.toExpression.mockReturnValue('my | expr'); const visualization = createMockVisualization(); - visualization.getPersistableState.mockImplementation((state) => ({ - things: `${state}_vis_persisted`, - })); + visualization.toExpression.mockReturnValue('vis | expr'); - const doc = await getSavedObjectFormat({ + const { doc, filterableIndexPatterns, isSaveable } = await getSavedObjectFormat({ ...saveArgs, activeDatasources: { indexpattern: datasource, @@ -74,27 +80,32 @@ describe('save editor frame state', () => { visualization, }); + expect(filterableIndexPatterns).toEqual([]); + expect(isSaveable).toEqual(true); expect(doc).toEqual({ id: undefined, - expression: '', state: { - datasourceMetaData: { - filterableIndexPatterns: [], - }, datasourceStates: { indexpattern: { stuff: '2_datasource_persisted', }, }, - visualization: { things: '4_vis_persisted' }, + visualization: '4', query: { query: '', language: 'lucene' }, filters: [ { - meta: { index: 'indexpattern' }, + meta: { indexRefName: 'filter-index-pattern-0' }, exists: { field: '@timestamp' }, }, ], }, + references: [ + { + id: 'indexpattern', + name: 'filter-index-pattern-0', + type: 'index-pattern', + }, + ], title: 'bbb', type: 'lens', visualizationType: '3', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index b41e93def966e0..6da6d5a8c118fe 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -5,11 +5,12 @@ */ import _ from 'lodash'; -import { toExpression } from '@kbn/interpreter/target/common'; +import { SavedObjectReference } from 'kibana/public'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; -import { buildExpression } from './expression_helpers'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; +import { extractFilterReferences } from '../../persistence'; +import { buildExpression } from './expression_helpers'; export interface Props { activeDatasources: Record; @@ -23,43 +24,55 @@ export function getSavedObjectFormat({ state, visualization, framePublicAPI, -}: Props): Document { +}: Props): { + doc: Document; + filterableIndexPatterns: string[]; + isSaveable: boolean; +} { + const datasourceStates: Record = {}; + const references: SavedObjectReference[] = []; + Object.entries(activeDatasources).forEach(([id, datasource]) => { + const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( + state.datasourceStates[id].state + ); + datasourceStates[id] = persistableState; + references.push(...savedObjectReferences); + }); + + const uniqueFilterableIndexPatternIds = _.uniq( + references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ); + + const { persistableFilters, references: filterReferences } = extractFilterReferences( + framePublicAPI.filters + ); + + references.push(...filterReferences); + const expression = buildExpression({ visualization, visualizationState: state.visualization.state, datasourceMap: activeDatasources, datasourceStates: state.datasourceStates, - framePublicAPI, - removeDateRange: true, - }); - - const datasourceStates: Record = {}; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - datasourceStates[id] = datasource.getPersistableState(state.datasourceStates[id].state); - }); - - const filterableIndexPatterns: Array<{ id: string; title: string }> = []; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - filterableIndexPatterns.push( - ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns - ); + datasourceLayers: framePublicAPI.datasourceLayers, }); return { - id: state.persistedId, - title: state.title, - description: state.description, - type: 'lens', - visualizationType: state.visualization.activeId, - expression: expression ? toExpression(expression) : '', - state: { - datasourceStates, - datasourceMetaData: { - filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'), + doc: { + id: state.persistedId, + title: state.title, + description: state.description, + type: 'lens', + visualizationType: state.visualization.activeId, + state: { + datasourceStates, + visualization: state.visualization.state, + query: framePublicAPI.query, + filters: persistableFilters, }, - visualization: visualization.getPersistableState(state.visualization.state), - query: framePublicAPI.query, - filters: framePublicAPI.filters, + references, }, + filterableIndexPatterns: uniqueFilterableIndexPatternIds, + isSaveable: expression !== null, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts new file mode 100644 index 00000000000000..6deb9ffd37a06f --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectReference } from 'kibana/public'; +import { Ast } from '@kbn/interpreter/common'; +import { Datasource, DatasourcePublicAPI, Visualization } from '../../types'; +import { buildExpression } from './expression_helpers'; +import { Document } from '../../persistence/saved_object_store'; + +export async function initializeDatasources( + datasourceMap: Record, + datasourceStates: Record, + references?: SavedObjectReference[] +) { + const states: Record = {}; + await Promise.all( + Object.entries(datasourceMap).map(([datasourceId, datasource]) => { + if (datasourceStates[datasourceId]) { + return datasource + .initialize(datasourceStates[datasourceId].state || undefined, references) + .then((datasourceState) => { + states[datasourceId] = { isLoading: false, state: datasourceState }; + }); + } + }) + ); + return states; +} + +export function createDatasourceLayers( + datasourceMap: Record, + datasourceStates: Record +) { + const datasourceLayers: Record = {}; + Object.keys(datasourceMap) + .filter((id) => datasourceStates[id] && !datasourceStates[id].isLoading) + .forEach((id) => { + const datasourceState = datasourceStates[id].state; + const datasource = datasourceMap[id]; + + const layers = datasource.getLayers(datasourceState); + layers.forEach((layer) => { + datasourceLayers[layer] = datasourceMap[id].getPublicAPI({ + state: datasourceState, + layerId: layer, + }); + }); + }); + return datasourceLayers; +} + +export async function persistedStateToExpression( + datasources: Record, + visualizations: Record, + doc: Document +): Promise { + const { + state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates }, + visualizationType, + references, + } = doc; + if (!visualizationType) return null; + const visualization = visualizations[visualizationType!]; + const datasourceStates = await initializeDatasources( + datasources, + Object.fromEntries( + Object.entries(persistedDatasourceStates).map(([id, state]) => [ + id, + { isLoading: false, state }, + ]) + ), + references + ); + + const datasourceLayers = createDatasourceLayers(datasources, datasourceStates); + + return buildExpression({ + visualization, + visualizationState, + datasourceMap: datasources, + datasourceStates, + datasourceLayers, + }); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 969467b5789ec0..c7f505aeca517b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -57,19 +57,16 @@ describe('editor_frame state management', () => { const initialState = getInitialState({ ...props, doc: { - expression: '', state: { datasourceStates: { testDatasource: { internalState1: '' }, testDatasource2: { internalState2: '' }, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], title: '', visualizationType: 'testVis', }, @@ -380,9 +377,7 @@ describe('editor_frame state management', () => { type: 'VISUALIZATION_LOADED', doc: { id: 'b', - expression: '', state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { a: { foo: 'c' } }, visualization: { bar: 'd' }, query: { query: '', language: 'lucene' }, @@ -392,6 +387,7 @@ describe('editor_frame state management', () => { description: 'My lens', type: 'lens', visualizationType: 'line', + references: [], }, } ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 263f7cd65f43d5..2bb1baf9d54f25 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -107,7 +107,7 @@ export function getSuggestions({ * title and preview expression. */ function getVisualizationSuggestions( - visualization: Visualization, + visualization: Visualization, table: TableSuggestion, visualizationId: string, datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index fd509c0046e13b..323472d7173523 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -249,7 +249,6 @@ describe('suggestion_panel', () => { expect(passedExpression).toMatchInlineSnapshot(` "kibana - | kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" filters=\\"[{\\\\\\"meta\\\\\\":{\\\\\\"index\\\\\\":\\\\\\"index1\\\\\\"},\\\\\\"exists\\\\\\":{\\\\\\"field\\\\\\":\\\\\\"myfield\\\\\\"}}]\\" | lens_merge_tables layerIds=\\"first\\" tables={datasource_expression} | test | expression" diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 7395075cf9f74a..f1dc3fa306d150 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -21,6 +21,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon'; import { Ast, toExpression } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; import { Action, PreviewState } from './state_management'; import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; @@ -28,7 +29,7 @@ import { ReactExpressionRendererProps, ReactExpressionRendererType, } from '../../../../../../src/plugins/expressions/public'; -import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; +import { prependDatasourceExpression } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; @@ -112,7 +113,7 @@ const SuggestionPreview = ({ }: { onSelect: () => void; preview: { - expression?: Ast; + expression?: Ast | null; icon: IconType; title: string; }; @@ -215,12 +216,24 @@ export function SuggestionPanel({ visualizationMap, ]); + const context: ExecutionContextSearch = useMemo( + () => ({ + query: frame.query, + timeRange: { + from: frame.dateRange.fromDate, + to: frame.dateRange.toDate, + }, + filters: frame.filters, + }), + [frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters] + ); + const AutoRefreshExpressionRenderer = useMemo(() => { const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$(); return (props: ReactExpressionRendererProps) => ( - + ); - }, [plugins.data.query.timefilter.timefilter]); + }, [plugins.data.query.timefilter.timefilter, context]); const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-1); @@ -252,15 +265,6 @@ export function SuggestionPanel({ } } - const expressionContext = { - query: frame.query, - filters: frame.filters, - timeRange: { - from: frame.dateRange.fromDate, - to: frame.dateRange.toDate, - }, - }; - return (

@@ -305,9 +309,7 @@ export function SuggestionPanel({ {currentVisualizationId && ( , + newVisualization: Visualization, subVisualizationId?: string ): Suggestion | undefined { const unfilteredSuggestions = getSuggestions({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index a9c638df8cad15..47e3b41df3b21b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -172,21 +172,6 @@ describe('workspace_panel', () => { "function": "kibana", "type": "function", }, - Object { - "arguments": Object { - "filters": Array [ - "[]", - ], - "query": Array [ - "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", - ], - "timeRange": Array [ - "{\\"from\\":\\"now-7d\\",\\"to\\":\\"now\\"}", - ], - }, - "function": "kibana_context", - "type": "function", - }, Object { "arguments": Object { "layerIds": Array [ @@ -305,10 +290,10 @@ describe('workspace_panel', () => { ); expect( - (instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.layerIds + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.layerIds ).toEqual(['first', 'second', 'third']); expect( - (instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.tables + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.tables ).toMatchInlineSnapshot(` Array [ Object { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index b3a12271f377b2..4f914bc65dc7c7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -18,6 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { CoreStart, CoreSetup } from 'kibana/public'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -129,7 +130,7 @@ export function InnerWorkspacePanel({ visualizationState, datasourceMap, datasourceStates, - framePublicAPI, + datasourceLayers: framePublicAPI.datasourceLayers, }); } catch (e) { // Most likely an error in the expression provided by a datasource or visualization @@ -173,6 +174,23 @@ export function InnerWorkspacePanel({ [plugins.data.query.timefilter.timefilter] ); + const context: ExecutionContextSearch = useMemo( + () => ({ + query: framePublicAPI.query, + timeRange: { + from: framePublicAPI.dateRange.fromDate, + to: framePublicAPI.dateRange.toDate, + }, + filters: framePublicAPI.filters, + }), + [ + framePublicAPI.query, + framePublicAPI.dateRange.fromDate, + framePublicAPI.dateRange.toDate, + framePublicAPI.filters, + ] + ); + useEffect(() => { // reset expression error if component attempts to run it again if (expression && localState.expressionBuildError) { @@ -264,6 +282,7 @@ export function InnerWorkspacePanel({ className="lnsExpressionRenderer__component" padding="m" expression={expression!} + searchContext={context} reload$={autoRefreshFetch$} onEvent={onEvent} renderError={(errorMessage?: string | null) => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 69447b3b9a9b83..1e2df28cad7b1a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -18,16 +18,13 @@ jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ })); const savedVis: Document = { - expression: 'my | expression', state: { visualization: {}, datasourceStates: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], title: 'My title', visualizationType: '', }; @@ -59,13 +56,14 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); embeddable.render(mountpoint); expect(expressionRenderer).toHaveBeenCalledTimes(1); - expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(savedVis.expression); + expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual('my | expression'); }); it('should re-render if new input is pushed', () => { @@ -82,6 +80,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); @@ -110,6 +109,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); @@ -117,11 +117,52 @@ describe('embeddable', () => { expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ timeRange, - query, + query: [query, savedVis.state.query], filters, }); }); + it('should merge external context with query and filters of the saved object', () => { + const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const query: Query = { language: 'kquery', query: 'external filter' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; + + const embeddable = new Embeddable( + dataPluginMock.createSetupContract().query.timefilter.timefilter, + expressionRenderer, + getTrigger, + { + editPath: '', + editUrl: '', + editable: true, + savedVis: { + ...savedVis, + state: { + ...savedVis.state, + query: { language: 'kquery', query: 'saved filter' }, + filters: [ + { meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } }, + ], + }, + references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }], + }, + expression: 'my | expression', + }, + { id: '123', timeRange, query, filters } + ); + embeddable.render(mountpoint); + + expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ + timeRange, + query: [query, { language: 'kquery', query: 'saved filter' }], + filters: [ + filters[0], + // actual index pattern id gets injected + { meta: { alias: 'test', negate: false, disabled: false, index: 'my-index-pattern-id' } }, + ], + }); + }); + it('should execute trigger on event from expression renderer', () => { const embeddable = new Embeddable( dataPluginMock.createSetupContract().query.timefilter.timefilter, @@ -132,6 +173,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); @@ -162,6 +204,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); @@ -195,6 +238,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index bbd2b18907e9b4..4df218a3e94e96 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -14,6 +14,7 @@ import { TimefilterContract, TimeRange, } from 'src/plugins/data/public'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; import { Subscription } from 'rxjs'; import { @@ -28,12 +29,13 @@ import { EmbeddableOutput, IContainer, } from '../../../../../../src/plugins/embeddable/public'; -import { DOC_TYPE, Document } from '../../persistence'; +import { DOC_TYPE, Document, injectFilterReferences } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, isLensFilterEvent } from '../../types'; export interface LensEmbeddableConfiguration { + expression: string | null; savedVis: Document; editUrl: string; editPath: string; @@ -56,12 +58,13 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(input)); this.onContainerStateChanged(initialInput); @@ -122,14 +133,14 @@ export class Embeddable extends AbstractEmbeddable !filter.meta.disabled) : undefined; if ( - !_.isEqual(containerState.timeRange, this.currentContext.timeRange) || - !_.isEqual(containerState.query, this.currentContext.query) || - !_.isEqual(cleanedFilters, this.currentContext.filters) + !_.isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || + !_.isEqual(containerState.query, this.externalSearchContext.query) || + !_.isEqual(cleanedFilters, this.externalSearchContext.filters) ) { - this.currentContext = { + this.externalSearchContext = { timeRange: containerState.timeRange, query: containerState.query, - lastReloadRequestTime: this.currentContext.lastReloadRequestTime, + lastReloadRequestTime: this.externalSearchContext.lastReloadRequestTime, filters: cleanedFilters, }; @@ -149,14 +160,37 @@ export class Embeddable extends AbstractEmbeddable, domNode ); } + /** + * Combines the embeddable context with the saved object context, and replaces + * any references to index patterns + */ + private getMergedSearchContext(): ExecutionContextSearch { + const output: ExecutionContextSearch = { + timeRange: this.externalSearchContext.timeRange, + }; + if (this.externalSearchContext.query) { + output.query = [this.externalSearchContext.query, this.savedVis.state.query]; + } else { + output.query = [this.savedVis.state.query]; + } + if (this.externalSearchContext.filters?.length) { + output.filters = [...this.externalSearchContext.filters, ...this.savedVis.state.filters]; + } else { + output.filters = [...this.savedVis.state.filters]; + } + + output.filters = injectFilterReferences(output.filters, this.savedVis.references); + return output; + } + handleEvent = (event: ExpressionRendererEvent) => { if (!this.getTrigger || this.input.disableTriggers) { return; @@ -188,9 +222,9 @@ export class Embeddable extends AbstractEmbeddable Promise; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { @@ -72,13 +75,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, timefilter, expressionRenderer, + documentToExpression, uiActions, } = await this.getStartServices(); const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); - const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( - async ({ id }) => { + const promises = savedVis.references + .filter(({ type }) => type === 'index-pattern') + .map(async ({ id }) => { try { return await indexPatternService.get(id); } catch (error) { @@ -87,14 +92,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { // to show. return null; } - } - ); + }); const indexPatterns = ( await Promise.all(promises) ).filter((indexPattern: IndexPattern | null): indexPattern is IndexPattern => Boolean(indexPattern) ); + const expression = await documentToExpression(savedVis); + return new Embeddable( timefilter, expressionRenderer, @@ -105,6 +111,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { editUrl: coreHttp.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`), editable: await this.isEditable(), indexPatterns, + expression: expression ? toExpression(expression) : null, }, input, parent diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index 296dcef3e70b92..d0d2360ddc1076 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -8,28 +8,23 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; -import { TimeRange, Filter, Query } from 'src/plugins/data/public'; import { ExpressionRendererEvent, ReactExpressionRendererType, } from 'src/plugins/expressions/public'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; expression: string | null; - context: { - timeRange?: TimeRange; - query?: Query; - filters?: Filter[]; - lastReloadRequestTime?: number; - }; + searchContext: ExecutionContextSearch; handleEvent: (event: ExpressionRendererEvent) => void; } export function ExpressionWrapper({ ExpressionRenderer: ExpressionRendererComponent, expression, - context, + searchContext, handleEvent, }: ExpressionWrapperProps) { return ( @@ -54,7 +49,7 @@ export function ExpressionWrapper({ className="lnsExpressionRenderer__component" padding="m" expression={expression} - searchContext={{ ...context }} + searchContext={searchContext} renderError={(error) =>
{error}
} onEvent={handleEvent} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 9c0825b3c2d270..86b137851d9bdd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -31,7 +31,6 @@ export function createMockVisualization(): jest.Mocked { getVisualizationTypeId: jest.fn((_state) => 'empty'), getDescription: jest.fn((_state) => ({ label: '' })), switchVisualizationType: jest.fn((_, x) => x), - getPersistableState: jest.fn((_state) => _state), getSuggestions: jest.fn((_options) => []), initialize: jest.fn((_frame, _state?) => ({})), getConfiguration: jest.fn((props) => ({ @@ -71,7 +70,7 @@ export function createMockDatasource(id: string): DatasourceMock { clearLayer: jest.fn((state, _layerId) => state), getDatasourceSuggestionsForField: jest.fn((_state, _item) => []), getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), - getPersistableState: jest.fn(), + getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [] })), getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), @@ -81,7 +80,6 @@ export function createMockDatasource(id: string): DatasourceMock { removeLayer: jest.fn((_state, _layerId) => {}), removeColumn: jest.fn((props) => {}), getLayers: jest.fn((_state) => []), - getMetaData: jest.fn((_state) => ({ filterableIndexPatterns: [] })), renderDimensionTrigger: jest.fn(), renderDimensionEditor: jest.fn(), diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 47339373b6d1ae..5fc347179a032b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -21,12 +21,14 @@ import { EditorFrameInstance, EditorFrameStart, } from '../types'; +import { Document } from '../persistence/saved_object_store'; import { EditorFrame } from './editor_frame'; import { mergeTables } from './merge_tables'; import { formatColumn } from './format_column'; import { EmbeddableFactory } from './embeddable/embeddable_factory'; import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { persistedStateToExpression } from './editor_frame/state_helpers'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; @@ -59,6 +61,21 @@ export class EditorFrameService { private readonly datasources: Array> = []; private readonly visualizations: Array> = []; + /** + * This method takes a Lens saved object as returned from the persistence helper, + * initializes datsources and visualization and creates the current expression. + * This is an asynchronous process and should only be triggered once for a saved object. + * @param doc parsed Lens saved object + */ + private async documentToExpression(doc: Document) { + const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ + collectAsyncDefinitions(this.datasources), + collectAsyncDefinitions(this.visualizations), + ]); + + return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc); + } + public setup( core: CoreSetup, plugins: EditorFrameSetupPlugins @@ -74,6 +91,7 @@ export class EditorFrameService { coreHttp: coreStart.http, timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, + documentToExpression: this.documentToExpression.bind(this), indexPatternService: deps.data.indexPatterns, uiActions: deps.uiActions, }; @@ -88,7 +106,7 @@ export class EditorFrameService { this.datasources.push(datasource as Datasource); }, registerVisualization: (visualization) => { - this.visualizations.push(visualization as Visualization); + this.visualizations.push(visualization as Visualization); }, }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts index ca5fe706985f85..c487e31f5a973a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts @@ -23,3 +23,7 @@ export function loadInitialState() { }; return result; } + +const originalLoader = jest.requireActual('../loader'); + +export const extractReferences = originalLoader.extractReferences; 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 dc3938ce436e53..0ba7b7df97853c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -128,12 +128,15 @@ const expectedIndexPatterns = { }, }; -function stateFromPersistedState( - persistedState: IndexPatternPersistedState -): IndexPatternPrivateState { +type IndexPatternBaseState = Omit< + IndexPatternPrivateState, + 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' +>; + +function enrichBaseState(baseState: IndexPatternBaseState): IndexPatternPrivateState { return { - currentIndexPatternId: persistedState.currentIndexPatternId, - layers: persistedState.layers, + currentIndexPatternId: baseState.currentIndexPatternId, + layers: baseState.layers, indexPatterns: expectedIndexPatterns, indexPatternRefs: [], existingFields: {}, @@ -142,7 +145,10 @@ function stateFromPersistedState( } describe('IndexPattern Data Source', () => { - let persistedState: IndexPatternPersistedState; + let baseState: Omit< + IndexPatternPrivateState, + 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' + >; let indexPatternDatasource: Datasource; beforeEach(() => { @@ -153,7 +159,7 @@ describe('IndexPattern Data Source', () => { charts: chartPluginMock.createSetupContract(), }); - persistedState = { + baseState = { currentIndexPatternId: '1', layers: { first: { @@ -224,9 +230,37 @@ describe('IndexPattern Data Source', () => { describe('#getPersistedState', () => { it('should persist from saved state', async () => { - const state = stateFromPersistedState(persistedState); + const state = enrichBaseState(baseState); - expect(indexPatternDatasource.getPersistableState(state)).toEqual(persistedState); + expect(indexPatternDatasource.getPersistableState(state)).toEqual({ + state: { + layers: { + first: { + columnOrder: ['col1'], + columns: { + col1: { + label: 'My Op', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'op', + params: { + size: 5, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + }, + }, + }, + }, + }, + savedObjectReferences: [ + { name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', id: '1' }, + { name: 'indexpattern-datasource-layer-first', type: 'index-pattern', id: '1' }, + ], + }); }); }); @@ -237,7 +271,7 @@ describe('IndexPattern Data Source', () => { }); it('should generate an expression for an aggregated query', async () => { - const queryPersistedState: IndexPatternPersistedState = { + const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', layers: { first: { @@ -266,7 +300,7 @@ describe('IndexPattern Data Source', () => { }, }; - const state = stateFromPersistedState(queryPersistedState); + const state = enrichBaseState(queryBaseState); expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(` Object { @@ -311,7 +345,7 @@ describe('IndexPattern Data Source', () => { }); it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { - const queryPersistedState: IndexPatternPersistedState = { + const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', layers: { first: { @@ -350,14 +384,14 @@ describe('IndexPattern Data Source', () => { }, }; - const state = stateFromPersistedState(queryPersistedState); + const state = enrichBaseState(queryBaseState); const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { - const queryPersistedState: IndexPatternPersistedState = { + const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', layers: { first: { @@ -386,7 +420,7 @@ describe('IndexPattern Data Source', () => { }, }; - const state = stateFromPersistedState(queryPersistedState); + const state = enrichBaseState(queryBaseState); const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']); @@ -489,55 +523,14 @@ describe('IndexPattern Data Source', () => { }); }); - describe('#getMetadata', () => { - it('should return the title of the index patterns', () => { - expect( - indexPatternDatasource.getMetaData({ - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, - layers: { - first: { - indexPatternId: '1', - columnOrder: [], - columns: {}, - }, - second: { - indexPatternId: '2', - columnOrder: [], - columns: {}, - }, - }, - currentIndexPatternId: '1', - }) - ).toEqual({ - filterableIndexPatterns: [ - { - id: '1', - title: 'my-fake-index-pattern', - }, - { - id: '2', - title: 'my-fake-restricted-pattern', - }, - ], - }); - }); - }); - describe('#getPublicAPI', () => { let publicAPI: DatasourcePublicAPI; beforeEach(async () => { - const initialState = stateFromPersistedState(persistedState); + const initialState = enrichBaseState(baseState); publicAPI = indexPatternDatasource.getPublicAPI({ state: initialState, layerId: 'first', - dateRange: { - fromDate: 'now-30d', - toDate: 'now', - }, }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2fb8d7fe0e553d..e2ca9335048498 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'kibana/public'; +import { CoreStart, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -19,7 +19,12 @@ import { DatasourceLayerPanelProps, PublicAPIProps, } from '../types'; -import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; +import { + loadInitialState, + changeIndexPattern, + changeLayerIndexPattern, + extractReferences, +} from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionTrigger, @@ -125,9 +130,13 @@ export function getIndexPatternDatasource({ const indexPatternDatasource: Datasource = { id: 'indexpattern', - async initialize(state?: IndexPatternPersistedState) { + async initialize( + persistedState?: IndexPatternPersistedState, + references?: SavedObjectReference[] + ) { return loadInitialState({ - state, + persistedState, + references, savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, @@ -135,8 +144,8 @@ export function getIndexPatternDatasource({ }); }, - getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) { - return { currentIndexPatternId, layers }; + getPersistableState(state: IndexPatternPrivateState) { + return extractReferences(state); }, insertLayer(state: IndexPatternPrivateState, newLayerId: string) { @@ -183,19 +192,6 @@ export function getIndexPatternDatasource({ toExpression, - getMetaData(state: IndexPatternPrivateState) { - return { - filterableIndexPatterns: _.uniq( - Object.values(state.layers) - .map((layer) => layer.indexPatternId) - .map((indexPatternId) => ({ - id: indexPatternId, - title: state.indexPatterns[indexPatternId].title, - })) - ), - }; - }, - renderDataPanel( domElement: Element, props: DatasourceDataPanelProps 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 cfabcb4edcef7e..d80bf779a5d17f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -12,6 +12,8 @@ import { changeIndexPattern, changeLayerIndexPattern, syncExistingFields, + extractReferences, + injectReferences, } from './loader'; import { IndexPatternsContract } from '../../../../../src/plugins/data/public'; import { @@ -378,10 +380,8 @@ describe('loader', () => { it('should initialize from saved state', async () => { const savedState: IndexPatternPersistedState = { - currentIndexPatternId: '2', layers: { layerb: { - indexPatternId: '2', columnOrder: ['col1', 'col2'], columns: { col1: { @@ -407,7 +407,12 @@ describe('loader', () => { }; const storage = createMockStorage({ indexPatternId: '1' }); const state = await loadInitialState({ - state: savedState, + persistedState: savedState, + references: [ + { name: 'indexpattern-datasource-current-indexpattern', id: '2', type: 'index-pattern' }, + { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, + { name: 'another-reference', id: 'c', type: 'index-pattern' }, + ], savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, @@ -422,7 +427,7 @@ describe('loader', () => { indexPatterns: { '2': sampleIndexPatterns['2'], }, - layers: savedState.layers, + layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -431,6 +436,79 @@ describe('loader', () => { }); }); + describe('saved object references', () => { + const state: IndexPatternPrivateState = { + currentIndexPatternId: 'b', + indexPatternRefs: [], + indexPatterns: {}, + existingFields: {}, + layers: { + a: { + indexPatternId: 'id-index-pattern-a', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'myfield', + }, + }, + }, + b: { + indexPatternId: 'id-index-pattern-b', + columnOrder: ['col2'], + columns: { + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'myfield2', + }, + }, + }, + }, + isFirstExistenceFetch: false, + }; + + it('should create a reference for each layer and for current index pattern', () => { + const { savedObjectReferences } = extractReferences(state); + expect(savedObjectReferences).toMatchInlineSnapshot(` + Array [ + Object { + "id": "b", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "id-index-pattern-a", + "name": "indexpattern-datasource-layer-a", + "type": "index-pattern", + }, + Object { + "id": "id-index-pattern-b", + "name": "indexpattern-datasource-layer-b", + "type": "index-pattern", + }, + ] + `); + }); + + it('should restore layers', () => { + const { savedObjectReferences, state: persistedState } = extractReferences(state); + expect(injectReferences(persistedState, savedObjectReferences).layers).toEqual(state.layers); + }); + + it('should restore current index pattern', () => { + const { savedObjectReferences, state: persistedState } = extractReferences(state); + expect(injectReferences(persistedState, savedObjectReferences).currentIndexPatternId).toEqual( + state.currentIndexPatternId + ); + }); + }); + describe('changeIndexPattern', () => { it('loads the index pattern and then sets it as current', async () => { const setState = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 9c4a19e58a0527..24906790a9fc9c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public'; import { StateSetter } from '../types'; import { IndexPattern, @@ -14,6 +14,7 @@ import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField, + IndexPatternLayer, } from './types'; import { updateLayerIndexPattern } from './state_helpers'; import { DateRange, ExistingFields } from '../../common/types'; @@ -115,14 +116,58 @@ const setLastUsedIndexPatternId = (storage: IStorageWrapper, value: string) => { writeToStorage(storage, 'indexPatternId', value); }; +const CURRENT_PATTERN_REFERENCE_NAME = 'indexpattern-datasource-current-indexpattern'; +function getLayerReferenceName(layerId: string) { + return `indexpattern-datasource-layer-${layerId}`; +} + +export function extractReferences({ currentIndexPatternId, layers }: IndexPatternPrivateState) { + const savedObjectReferences: SavedObjectReference[] = []; + savedObjectReferences.push({ + type: 'index-pattern', + id: currentIndexPatternId, + name: CURRENT_PATTERN_REFERENCE_NAME, + }); + const persistableLayers: Record> = {}; + Object.entries(layers).forEach(([layerId, { indexPatternId, ...persistableLayer }]) => { + savedObjectReferences.push({ + type: 'index-pattern', + id: indexPatternId, + name: getLayerReferenceName(layerId), + }); + persistableLayers[layerId] = persistableLayer; + }); + return { savedObjectReferences, state: { layers: persistableLayers } }; +} + +export function injectReferences( + state: IndexPatternPersistedState, + references: SavedObjectReference[] +) { + const layers: Record = {}; + Object.entries(state.layers).forEach(([layerId, persistedLayer]) => { + layers[layerId] = { + ...persistedLayer, + indexPatternId: references.find(({ name }) => name === getLayerReferenceName(layerId))!.id, + }; + }); + return { + currentIndexPatternId: references.find(({ name }) => name === CURRENT_PATTERN_REFERENCE_NAME)! + .id, + layers, + }; +} + export async function loadInitialState({ - state, + persistedState, + references, savedObjectsClient, defaultIndexPatternId, storage, indexPatternsService, }: { - state?: IndexPatternPersistedState; + persistedState?: IndexPatternPersistedState; + references?: SavedObjectReference[]; savedObjectsClient: SavedObjectsClient; defaultIndexPatternId?: string; storage: IStorageWrapper; @@ -131,6 +176,9 @@ export async function loadInitialState({ const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); + const state = + persistedState && references ? injectReferences(persistedState, references) : undefined; + const requiredPatterns = _.uniq( state ? Object.values(state.layers) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 8d0e82b176aa97..95cc47e68f8a15 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -40,11 +40,12 @@ export interface IndexPatternLayer { } export interface IndexPatternPersistedState { - currentIndexPatternId: string; - layers: Record; + layers: Record>; } -export type IndexPatternPrivateState = IndexPatternPersistedState & { +export interface IndexPatternPrivateState { + currentIndexPatternId: string; + layers: Record; indexPatternRefs: IndexPatternRef[]; indexPatterns: Record; @@ -54,7 +55,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { existingFields: Record>; isFirstExistenceFetch: boolean; existenceFetchFailed?: boolean; -}; +} export interface IndexPatternRef { id: string; diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 62f47a21c85b01..f3c9a725ee2e2b 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -66,12 +66,6 @@ describe('metric_visualization', () => { }); }); - describe('#getPersistableState', () => { - it('persists the state as given', () => { - expect(metricVisualization.getPersistableState(exampleState())).toEqual(exampleState()); - }); - }); - describe('#getConfiguration', () => { it('can add a metric when there is no accessor', () => { expect( @@ -168,7 +162,8 @@ describe('metric_visualization', () => { datasourceLayers: { l1: datasource }, }; - expect(metricVisualization.toExpression(exampleState(), frame)).toMatchInlineSnapshot(` + expect(metricVisualization.toExpression(exampleState(), frame.datasourceLayers)) + .toMatchInlineSnapshot(` Object { "chain": Array [ Object { diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index e565d2fa8b293b..5f1ce5334dd36c 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -7,20 +7,20 @@ import { i18n } from '@kbn/i18n'; import { Ast } from '@kbn/interpreter/target/common'; import { getSuggestions } from './metric_suggestions'; -import { Visualization, FramePublicAPI, OperationMetadata } from '../types'; -import { State, PersistableState } from './types'; +import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types'; +import { State } from './types'; import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( state: State, - frame: FramePublicAPI, + datasourceLayers: Record, mode: 'reduced' | 'full' = 'full' ): Ast | null => { if (!state.accessor) { return null; } - const [datasource] = Object.values(frame.datasourceLayers); + const [datasource] = Object.values(datasourceLayers); const operation = datasource && datasource.getOperationForColumnId(state.accessor); return { @@ -39,7 +39,7 @@ const toExpression = ( }; }; -export const metricVisualization: Visualization = { +export const metricVisualization: Visualization = { id: 'lnsMetric', visualizationTypes: [ @@ -88,8 +88,6 @@ export const metricVisualization: Visualization = { ); }, - getPersistableState: (state) => state, - getConfiguration(props) { return { groups: [ @@ -106,8 +104,8 @@ export const metricVisualization: Visualization = { }, toExpression, - toPreviewExpression: (state: State, frame: FramePublicAPI) => - toExpression(state, frame, 'reduced'), + toPreviewExpression: (state, datasourceLayers) => + toExpression(state, datasourceLayers, 'reduced'), setDimension({ prevState, columnId }) { return { ...prevState, accessor: columnId }; diff --git a/x-pack/plugins/lens/public/metric_visualization/types.ts b/x-pack/plugins/lens/public/metric_visualization/types.ts index 53fc1039342558..86a781716b3457 100644 --- a/x-pack/plugins/lens/public/metric_visualization/types.ts +++ b/x-pack/plugins/lens/public/metric_visualization/types.ts @@ -13,5 +13,3 @@ export interface MetricConfig extends State { title: string; mode: 'reduced' | 'full'; } - -export type PersistableState = State; diff --git a/x-pack/plugins/lens/public/persistence/filter_references.test.ts b/x-pack/plugins/lens/public/persistence/filter_references.test.ts new file mode 100644 index 00000000000000..23c0cd1d11f1b6 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/filter_references.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { Filter } from 'src/plugins/data/public'; +import { extractFilterReferences, injectFilterReferences } from './filter_references'; +import { FilterStateStore } from 'src/plugins/data/common'; + +describe('filter saved object references', () => { + const filters: Filter[] = [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.src', + negate: true, + params: { query: 'CN' }, + type: 'phrase', + }, + query: { match_phrase: { 'geo.src': 'CN' } }, + }, + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + key: 'geoip.country_iso_code', + negate: true, + params: { query: 'US' }, + type: 'phrase', + }, + query: { match_phrase: { 'geoip.country_iso_code': 'US' } }, + }, + ]; + + it('should create two index-pattern references', () => { + const { references } = extractFilterReferences(filters); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "filter-index-pattern-0", + "type": "index-pattern", + }, + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "filter-index-pattern-1", + "type": "index-pattern", + }, + ] + `); + }); + + it('should restore the same filter after extracting and injecting', () => { + const { persistableFilters, references } = extractFilterReferences(filters); + expect(injectFilterReferences(persistableFilters, references)).toEqual(filters); + }); + + it('should ignore other references', () => { + const { persistableFilters, references } = extractFilterReferences(filters); + expect( + injectFilterReferences(persistableFilters, [ + { type: 'index-pattern', id: '1234', name: 'some other index pattern' }, + ...references, + ]) + ).toEqual(filters); + }); + + it('should inject other ids if references change', () => { + const { persistableFilters, references } = extractFilterReferences(filters); + + expect( + injectFilterReferences( + persistableFilters, + references.map((reference, index) => ({ ...reference, id: `overwritten-id-${index}` })) + ) + ).toEqual([ + { + ...filters[0], + meta: { + ...filters[0].meta, + index: 'overwritten-id-0', + }, + }, + { + ...filters[1], + meta: { + ...filters[1].meta, + index: 'overwritten-id-1', + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/lens/public/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts new file mode 100644 index 00000000000000..47564e510ce9c8 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -0,0 +1,56 @@ +/* + * 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 { Filter } from 'src/plugins/data/public'; +import { SavedObjectReference } from 'kibana/public'; +import { PersistableFilter } from '../../common'; + +export function extractFilterReferences( + filters: Filter[] +): { persistableFilters: PersistableFilter[]; references: SavedObjectReference[] } { + const references: SavedObjectReference[] = []; + const persistableFilters = filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `filter-index-pattern-${i}`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }); + + return { persistableFilters, references }; +} + +export function injectFilterReferences( + filters: PersistableFilter[], + references: SavedObjectReference[] +) { + return filters.map((filterRow) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return filterRow as Filter; + } + const { indexRefName, ...metaRest } = filterRow.meta; + const reference = references.find((ref) => ref.name === indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${indexRefName}`); + } + return { + ...filterRow, + meta: { ...metaRest, index: reference.id }, + }; + }); +} diff --git a/x-pack/plugins/lens/public/persistence/index.ts b/x-pack/plugins/lens/public/persistence/index.ts index 1f823ff75c8c63..464bd467904227 100644 --- a/x-pack/plugins/lens/public/persistence/index.ts +++ b/x-pack/plugins/lens/public/persistence/index.ts @@ -5,3 +5,4 @@ */ export * from './saved_object_store'; +export * from './filter_references'; diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts index f8f8d889233a7d..ba7c0ee6ae786e 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts @@ -30,11 +30,8 @@ describe('LensStore', () => { title: 'Hello', description: 'My doc', visualizationType: 'bar', - expression: '', + references: [], state: { - datasourceMetaData: { - filterableIndexPatterns: [], - }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, @@ -49,11 +46,8 @@ describe('LensStore', () => { title: 'Hello', description: 'My doc', visualizationType: 'bar', - expression: '', + references: [], state: { - datasourceMetaData: { - filterableIndexPatterns: [], - }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, @@ -64,21 +58,25 @@ describe('LensStore', () => { }); expect(client.create).toHaveBeenCalledTimes(1); - expect(client.create).toHaveBeenCalledWith('lens', { - title: 'Hello', - description: 'My doc', - visualizationType: 'bar', - expression: '', - state: { - datasourceMetaData: { filterableIndexPatterns: [] }, - datasourceStates: { - indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + expect(client.create).toHaveBeenCalledWith( + 'lens', + { + title: 'Hello', + description: 'My doc', + visualizationType: 'bar', + state: { + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + }, + visualization: { x: 'foo', y: 'baz' }, + query: { query: '', language: 'lucene' }, + filters: [], }, - visualization: { x: 'foo', y: 'baz' }, - query: { query: '', language: 'lucene' }, - filters: [], }, - }); + { + references: [], + } + ); }); test('updates and returns a visualization document', async () => { @@ -87,9 +85,8 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, @@ -101,9 +98,8 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, @@ -116,22 +112,21 @@ describe('LensStore', () => { { type: 'lens', id: 'Gandalf', + references: [], attributes: { title: null, visualizationType: null, - expression: null, state: null, }, }, { type: 'lens', id: 'Gandalf', + references: [], attributes: { title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index 59ead53956a8d8..e4609213ec7929 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/public'; -import { Query, Filter } from '../../../../../src/plugins/data/public'; +import { + SavedObjectAttributes, + SavedObjectsClientContract, + SavedObjectReference, +} from 'kibana/public'; +import { Query } from '../../../../../src/plugins/data/public'; +import { PersistableFilter } from '../../common'; export interface Document { id?: string; @@ -13,16 +18,13 @@ export interface Document { visualizationType: string | null; title: string; description?: string; - expression: string | null; state: { - datasourceMetaData: { - filterableIndexPatterns: Array<{ id: string; title: string }>; - }; datasourceStates: Record; visualization: unknown; query: Query; - filters: Filter[]; + filters: PersistableFilter[]; }; + references: SavedObjectReference[]; } export const DOC_TYPE = 'lens'; @@ -45,14 +47,16 @@ export class SavedObjectIndexStore implements SavedObjectStore { } async save(vis: Document) { - const { id, type, ...rest } = vis; + const { id, type, references, ...rest } = vis; // TODO: SavedObjectAttributes should support this kind of object, // remove this workaround when SavedObjectAttributes is updated. const attributes = (rest as unknown) as SavedObjectAttributes; const result = await (id - ? this.safeUpdate(id, attributes) - : this.client.create(DOC_TYPE, attributes)); + ? this.safeUpdate(id, attributes, references) + : this.client.create(DOC_TYPE, attributes, { + references, + })); return { ...vis, id: result.id }; } @@ -63,21 +67,25 @@ export class SavedObjectIndexStore implements SavedObjectStore { // deleted subtrees make it back into the object after a load. // This function fixes this by doing two updates - one to empty out the document setting // every key to null, and a second one to load the new content. - private async safeUpdate(id: string, attributes: SavedObjectAttributes) { + private async safeUpdate( + id: string, + attributes: SavedObjectAttributes, + references: SavedObjectReference[] + ) { const resetAttributes: SavedObjectAttributes = {}; Object.keys(attributes).forEach((key) => { resetAttributes[key] = null; }); return ( await this.client.bulkUpdate([ - { type: DOC_TYPE, id, attributes: resetAttributes }, - { type: DOC_TYPE, id, attributes }, + { type: DOC_TYPE, id, attributes: resetAttributes, references }, + { type: DOC_TYPE, id, attributes, references }, ]) ).savedObjects[1]; } async load(id: string): Promise { - const { type, attributes, error } = await this.client.get(DOC_TYPE, id); + const { type, attributes, references, error } = await this.client.get(DOC_TYPE, id); if (error) { throw error; @@ -85,6 +93,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { return { ...(attributes as SavedObjectAttributes), + references, id, type, } as Document; diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx index 5a68516db6aa3f..855bacd4f794c0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx @@ -31,7 +31,7 @@ const bucketedOperations = (op: OperationMetadata) => op.isBucketed; const numberMetricOperations = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; -export const pieVisualization: Visualization = { +export const pieVisualization: Visualization = { id: 'lnsPie', visualizationTypes: [ @@ -91,8 +91,6 @@ export const pieVisualization: Visualization state, - getSuggestions: suggestions, getConfiguration({ state, frame, layerId }) { diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index fbc47e8bfb00f6..f36b9efb930a93 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -5,21 +5,24 @@ */ import { Ast } from '@kbn/interpreter/common'; -import { FramePublicAPI, Operation } from '../types'; +import { Operation, DatasourcePublicAPI } from '../types'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { PieVisualizationState } from './types'; -export function toExpression(state: PieVisualizationState, frame: FramePublicAPI) { - return expressionHelper(state, frame, false); +export function toExpression( + state: PieVisualizationState, + datasourceLayers: Record +) { + return expressionHelper(state, datasourceLayers, false); } function expressionHelper( state: PieVisualizationState, - frame: FramePublicAPI, + datasourceLayers: Record, isPreview: boolean ): Ast | null { const layer = state.layers[0]; - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; const operations = layer.groups .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); @@ -50,6 +53,9 @@ function expressionHelper( }; } -export function toPreviewExpression(state: PieVisualizationState, frame: FramePublicAPI) { - return expressionHelper(state, frame, true); +export function toPreviewExpression( + state: PieVisualizationState, + datasourceLayers: Record +) { + return expressionHelper(state, datasourceLayers, true); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index c7bda65cd13273..20f2ce6c567740 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -7,6 +7,7 @@ import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'kibana/public'; +import { SavedObjectReference } from 'kibana/public'; import { ExpressionRendererEvent, IInterpreterRenderHandlers, @@ -30,7 +31,6 @@ export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; export interface PublicAPIProps { state: T; layerId: string; - dateRange: DateRange; } export interface EditorFrameProps { @@ -44,8 +44,9 @@ export interface EditorFrameProps { // Frame loader (app or embeddable) is expected to call this when it loads and updates // This should be replaced with a top-down state onChange: (newState: { - filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + filterableIndexPatterns: string[]; doc: Document; + isSaveable: boolean; }) => void; showNoDataPopover: () => void; } @@ -57,9 +58,7 @@ export interface EditorFrameInstance { export interface EditorFrameSetup { // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation registerDatasource: (datasource: Datasource | Promise>) => void; - registerVisualization: ( - visualization: Visualization | Promise> - ) => void; + registerVisualization: (visualization: Visualization | Promise>) => void; } export interface EditorFrameStart { @@ -131,10 +130,6 @@ export interface DatasourceSuggestion { keptLayerIds: string[]; } -export interface DatasourceMetaData { - filterableIndexPatterns: Array<{ id: string; title: string }>; -} - export type StateSetter = (newState: T | ((prevState: T) => T)) => void; /** @@ -146,10 +141,10 @@ export interface Datasource { // For initializing, either from an empty state or from persisted state // Because this will be called at runtime, state might have a type of `any` and // datasources should validate their arguments - initialize: (state?: P) => Promise; + initialize: (state?: P, savedObjectReferences?: SavedObjectReference[]) => Promise; // Given the current state, which parts should be saved? - getPersistableState: (state: T) => P; + getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; insertLayer: (state: T, newLayerId: string) => T; removeLayer: (state: T, layerId: string) => T; @@ -166,8 +161,6 @@ export interface Datasource { toExpression: (state: T, layerId: string) => Ast | string | null; - getMetaData: (state: T) => DatasourceMetaData; - getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; @@ -408,7 +401,7 @@ export interface VisualizationType { label: string; } -export interface Visualization { +export interface Visualization { /** Plugin ID, such as "lnsXY" */ id: string; @@ -418,11 +411,7 @@ export interface Visualization { * - Loadingn from a saved visualization * - When using suggestions, the suggested state is passed in */ - initialize: (frame: FramePublicAPI, state?: P) => T; - /** - * Can remove any state that should not be persisted to saved object, such as UI state - */ - getPersistableState: (state: T) => P; + initialize: (frame: FramePublicAPI, state?: T) => T; /** * Visualizations must provide at least one type for the chart switcher, @@ -504,12 +493,18 @@ export interface Visualization { */ getSuggestions: (context: SuggestionRequest) => Array>; - toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; + toExpression: ( + state: T, + datasourceLayers: Record + ) => Ast | string | null; /** * Expression to render a preview version of the chart in very constrained space. * If there is no expression provided, the preview icon is used. */ - toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; + toPreviewExpression?: ( + state: T, + datasourceLayers: Record + ) => Ast | string | null; } export interface LensFilterEvent { diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 876d1141740e16..f579085646f6f9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -53,7 +53,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toMatchSnapshot(); }); @@ -74,7 +74,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast).chain[0].arguments.fittingFunction[0] ).toEqual('None'); }); @@ -94,7 +94,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast; expect(expression.chain[0].arguments.showXAxisTitle[0]).toBe(true); expect(expression.chain[0].arguments.showYAxisTitle[0]).toBe(true); @@ -116,7 +116,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toBeNull(); }); @@ -137,7 +137,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toBeNull(); }); @@ -157,7 +157,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers )! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); @@ -191,7 +191,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast; expect( (expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments @@ -216,7 +216,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast; expect( (expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 9b9c159af265e0..cd32d4f94c3e51 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -7,13 +7,16 @@ import { Ast } from '@kbn/interpreter/common'; import { ScaleType } from '@elastic/charts'; import { State, LayerConfig } from './types'; -import { FramePublicAPI, OperationMetadata } from '../types'; +import { OperationMetadata, DatasourcePublicAPI } from '../types'; interface ValidLayer extends LayerConfig { xAccessor: NonNullable; } -export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => { +export const toExpression = ( + state: State, + datasourceLayers: Record +): Ast | null => { if (!state || !state.layers.length) { return null; } @@ -21,19 +24,20 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => const metadata: Record> = {}; state.layers.forEach((layer) => { metadata[layer.layerId] = {}; - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; datasource.getTableSpec().forEach((column) => { - const operation = frame.datasourceLayers[layer.layerId].getOperationForColumnId( - column.columnId - ); + const operation = datasourceLayers[layer.layerId].getOperationForColumnId(column.columnId); metadata[layer.layerId][column.columnId] = operation; }); }); - return buildExpression(state, metadata, frame); + return buildExpression(state, metadata, datasourceLayers); }; -export function toPreviewExpression(state: State, frame: FramePublicAPI) { +export function toPreviewExpression( + state: State, + datasourceLayers: Record +) { return toExpression( { ...state, @@ -44,7 +48,7 @@ export function toPreviewExpression(state: State, frame: FramePublicAPI) { isVisible: false, }, }, - frame + datasourceLayers ); } @@ -77,7 +81,7 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S export const buildExpression = ( state: State, metadata: Record>, - frame?: FramePublicAPI + datasourceLayers?: Record ): Ast | null => { const validLayers = state.layers.filter((layer): layer is ValidLayer => Boolean(layer.xAccessor && layer.accessors.length) @@ -149,8 +153,8 @@ export const buildExpression = ( layers: validLayers.map((layer) => { const columnToLabel: Record = {}; - if (frame) { - const datasource = frame.datasourceLayers[layer.layerId]; + if (datasourceLayers) { + const datasource = datasourceLayers[layer.layerId]; layer.accessors .concat(layer.splitAccessor ? [layer.splitAccessor] : []) .forEach((accessor) => { @@ -162,8 +166,8 @@ export const buildExpression = ( } const xAxisOperation = - frame && - frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); + datasourceLayers && + datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); const isHistogramDimension = Boolean( xAxisOperation && diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index ab689ceb183bea..2739ffe42f13fc 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -339,7 +339,6 @@ export interface XYState { } export type State = XYState; -export type PersistableState = XYState; export const visualizationTypes: VisualizationType[] = [ { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 0a8e8bbe0c46fc..53f7a23dcae985 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -157,12 +157,6 @@ describe('xy_visualization', () => { }); }); - describe('#getPersistableState', () => { - it('persists the state as given', () => { - expect(xyVisualization.getPersistableState(exampleState())).toEqual(exampleState()); - }); - }); - describe('#removeLayer', () => { it('removes the specified layer', () => { const prevState: State = { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index f321e0962caa87..8c551c575764e4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import { Visualization, OperationMetadata, VisualizationType } from '../types'; -import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; +import { State, SeriesType, visualizationTypes, LayerConfig } from './types'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; @@ -74,7 +74,7 @@ function getDescription(state?: State) { }; } -export const xyVisualization: Visualization = { +export const xyVisualization: Visualization = { id: 'lnsXY', visualizationTypes, @@ -159,8 +159,6 @@ export const xyVisualization: Visualization = { ); }, - getPersistableState: (state) => state, - getConfiguration(props) { const layer = props.state.layers.find((l) => l.layerId === props.layerId)!; return { diff --git a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap new file mode 100644 index 00000000000000..4979438dbd3d04 --- /dev/null +++ b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Lens migrations 7.10.0 references should produce a valid document 1`] = ` +Object { + "attributes": Object { + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "3b7791e9-326e-40d5-a787-b7594e48d906": Object { + "columnOrder": Array [ + "77d8383e-f66e-471e-ae50-c427feedb5ba", + "a5c1b82d-51de-4448-a99d-6391432c3a03", + ], + "columns": Object { + "77d8383e-f66e-471e-ae50-c427feedb5ba": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geoip.country_iso_code", + "operationType": "terms", + "params": Object { + "orderBy": Object { + "columnId": "a5c1b82d-51de-4448-a99d-6391432c3a03", + "type": "column", + }, + "orderDirection": "desc", + "size": 5, + }, + "scale": "ordinal", + "sourceField": "geoip.country_iso_code", + }, + "a5c1b82d-51de-4448-a99d-6391432c3a03": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "Records", + }, + }, + }, + "9a27f85d-35a9-4246-81b2-48e7ee9b0707": Object { + "columnOrder": Array [ + "96352896-c508-4fca-90d8-66e9ebfce621", + "4ce9b4c7-2ebf-4d48-8669-0ea69d973353", + ], + "columns": Object { + "4ce9b4c7-2ebf-4d48-8669-0ea69d973353": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "Records", + }, + "96352896-c508-4fca-90d8-66e9ebfce621": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.src", + "operationType": "terms", + "params": Object { + "orderBy": Object { + "columnId": "4ce9b4c7-2ebf-4d48-8669-0ea69d973353", + "type": "column", + }, + "orderDirection": "desc", + "size": 5, + }, + "scale": "ordinal", + "sourceField": "geo.src", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "indexRefName": "filter-index-pattern-0", + "key": "geo.src", + "negate": true, + "params": Object { + "query": "CN", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "geo.src": "CN", + }, + }, + }, + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "indexRefName": "filter-index-pattern-1", + "key": "geoip.country_iso_code", + "negate": true, + "params": Object { + "query": "US", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "geoip.country_iso_code": "US", + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "NOT bytes > 5000", + }, + "visualization": Object { + "fittingFunction": "None", + "layers": Array [ + Object { + "accessors": Array [ + "4ce9b4c7-2ebf-4d48-8669-0ea69d973353", + ], + "layerId": "9a27f85d-35a9-4246-81b2-48e7ee9b0707", + "position": "top", + "seriesType": "bar", + "showGridlines": false, + "xAccessor": "96352896-c508-4fca-90d8-66e9ebfce621", + }, + Object { + "accessors": Array [ + "a5c1b82d-51de-4448-a99d-6391432c3a03", + ], + "layerId": "3b7791e9-326e-40d5-a787-b7594e48d906", + "seriesType": "bar", + "xAccessor": "77d8383e-f66e-471e-ae50-c427feedb5ba", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar", + }, + }, + "title": "mylens", + "visualizationType": "lnsXY", + }, + "references": Array [ + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906", + "type": "index-pattern", + }, + Object { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707", + "type": "index-pattern", + }, + Object { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "filter-index-pattern-0", + "type": "index-pattern", + }, + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "filter-index-pattern-1", + "type": "index-pattern", + }, + ], + "type": "lens", +} +`; diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index 0541d9636577ba..676494dcab619b 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -278,4 +278,233 @@ describe('Lens migrations', () => { expect(result).toEqual(input); }); }); + + describe('7.10.0 references', () => { + const context = {} as SavedObjectMigrationContext; + + const example = { + attributes: { + description: '', + expression: + 'kibana\n| kibana_context query="{\\"query\\":\\"NOT bytes > 5000\\",\\"language\\":\\"kuery\\"}" \n filters="[{\\"meta\\":{\\"index\\":\\"90943e30-9a47-11e8-b64d-95841ca0b247\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geo.src\\",\\"params\\":{\\"query\\":\\"CN\\"}},\\"query\\":{\\"match_phrase\\":{\\"geo.src\\":\\"CN\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}},{\\"meta\\":{\\"index\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geoip.country_iso_code\\",\\"params\\":{\\"query\\":\\"US\\"}},\\"query\\":{\\"match_phrase\\":{\\"geoip.country_iso_code\\":\\"US\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}]"\n| lens_merge_tables layerIds="9a27f85d-35a9-4246-81b2-48e7ee9b0707"\n layerIds="3b7791e9-326e-40d5-a787-b7594e48d906" \n tables={esaggs index="90943e30-9a47-11e8-b64d-95841ca0b247" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geo.src\\",\\"orderBy\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-96352896-c508-4fca-90d8-66e9ebfce621\\":{\\"label\\":\\"Top values of geo.src\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geo.src\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\"},\\"col-1-4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"}}"}\n tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geoip.country_iso_code\\",\\"orderBy\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-77d8383e-f66e-471e-ae50-c427feedb5ba\\":{\\"label\\":\\"Top values of geoip.country_iso_code\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geoip.country_iso_code\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\"},\\"col-1-a5c1b82d-51de-4448-a99d-6391432c3a03\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"}}"}\n| lens_xy_chart xTitle="Top values of geo.src" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} fittingFunction="None" \n layers={lens_xy_layer layerId="9a27f85d-35a9-4246-81b2-48e7ee9b0707" hide=false xAccessor="96352896-c508-4fca-90d8-66e9ebfce621" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="4ce9b4c7-2ebf-4d48-8669-0ea69d973353" columnToLabel="{\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":\\"Count of records\\"}"}\n layers={lens_xy_layer layerId="3b7791e9-326e-40d5-a787-b7594e48d906" hide=false xAccessor="77d8383e-f66e-471e-ae50-c427feedb5ba" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="a5c1b82d-51de-4448-a99d-6391432c3a03" columnToLabel="{\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\":\\"Count of records [1]\\"}"}', + state: { + datasourceMetaData: { + filterableIndexPatterns: [ + { id: '90943e30-9a47-11e8-b64d-95841ca0b247', title: 'kibana_sample_data_logs' }, + { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' }, + ], + }, + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + layers: { + '3b7791e9-326e-40d5-a787-b7594e48d906': { + columnOrder: [ + '77d8383e-f66e-471e-ae50-c427feedb5ba', + 'a5c1b82d-51de-4448-a99d-6391432c3a03', + ], + columns: { + '77d8383e-f66e-471e-ae50-c427feedb5ba': { + dataType: 'string', + isBucketed: true, + label: 'Top values of geoip.country_iso_code', + operationType: 'terms', + params: { + orderBy: { + columnId: 'a5c1b82d-51de-4448-a99d-6391432c3a03', + type: 'column', + }, + orderDirection: 'desc', + size: 5, + }, + scale: 'ordinal', + sourceField: 'geoip.country_iso_code', + }, + 'a5c1b82d-51de-4448-a99d-6391432c3a03': { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + }, + '9a27f85d-35a9-4246-81b2-48e7ee9b0707': { + columnOrder: [ + '96352896-c508-4fca-90d8-66e9ebfce621', + '4ce9b4c7-2ebf-4d48-8669-0ea69d973353', + ], + columns: { + '4ce9b4c7-2ebf-4d48-8669-0ea69d973353': { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + '96352896-c508-4fca-90d8-66e9ebfce621': { + dataType: 'string', + isBucketed: true, + label: 'Top values of geo.src', + operationType: 'terms', + params: { + orderBy: { + columnId: '4ce9b4c7-2ebf-4d48-8669-0ea69d973353', + type: 'column', + }, + orderDirection: 'desc', + size: 5, + }, + scale: 'ordinal', + sourceField: 'geo.src', + }, + }, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + }, + }, + }, + }, + filters: [ + { + $state: { store: 'appState' }, + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.src', + negate: true, + params: { query: 'CN' }, + type: 'phrase', + }, + query: { match_phrase: { 'geo.src': 'CN' } }, + }, + { + $state: { store: 'appState' }, + meta: { + alias: null, + disabled: false, + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + key: 'geoip.country_iso_code', + negate: true, + params: { query: 'US' }, + type: 'phrase', + }, + query: { match_phrase: { 'geoip.country_iso_code': 'US' } }, + }, + ], + query: { language: 'kuery', query: 'NOT bytes > 5000' }, + visualization: { + fittingFunction: 'None', + layers: [ + { + accessors: ['4ce9b4c7-2ebf-4d48-8669-0ea69d973353'], + layerId: '9a27f85d-35a9-4246-81b2-48e7ee9b0707', + position: 'top', + seriesType: 'bar', + showGridlines: false, + xAccessor: '96352896-c508-4fca-90d8-66e9ebfce621', + }, + { + accessors: ['a5c1b82d-51de-4448-a99d-6391432c3a03'], + layerId: '3b7791e9-326e-40d5-a787-b7594e48d906', + seriesType: 'bar', + xAccessor: '77d8383e-f66e-471e-ae50-c427feedb5ba', + }, + ], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'bar', + }, + }, + title: 'mylens', + visualizationType: 'lnsXY', + }, + type: 'lens', + }; + + it('should remove expression', () => { + const result = migrations['7.10.0'](example, context); + expect(result.attributes.expression).toBeUndefined(); + }); + + it('should list references for layers', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.references?.find( + (ref) => ref.name === 'indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906' + )?.id + ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f'); + expect( + result.references?.find( + (ref) => ref.name === 'indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707' + )?.id + ).toEqual('90943e30-9a47-11e8-b64d-95841ca0b247'); + }); + + it('should remove index pattern ids from layers', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.attributes.state.datasourceStates.indexpattern.layers[ + '3b7791e9-326e-40d5-a787-b7594e48d906' + ].indexPatternId + ).toBeUndefined(); + expect( + result.attributes.state.datasourceStates.indexpattern.layers[ + '9a27f85d-35a9-4246-81b2-48e7ee9b0707' + ].indexPatternId + ).toBeUndefined(); + }); + + it('should remove datsource meta data', () => { + const result = migrations['7.10.0'](example, context); + expect(result.attributes.state.datasourceMetaData).toBeUndefined(); + }); + + it('should list references for filters', () => { + const result = migrations['7.10.0'](example, context); + expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-0')?.id).toEqual( + '90943e30-9a47-11e8-b64d-95841ca0b247' + ); + expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-1')?.id).toEqual( + 'ff959d40-b880-11e8-a6d9-e546fe2bba5f' + ); + }); + + it('should remove index pattern ids from filters', () => { + const result = migrations['7.10.0'](example, context); + expect(result.attributes.state.filters[0].meta.index).toBeUndefined(); + expect(result.attributes.state.filters[0].meta.indexRefName).toEqual( + 'filter-index-pattern-0' + ); + expect(result.attributes.state.filters[1].meta.index).toBeUndefined(); + expect(result.attributes.state.filters[1].meta.indexRefName).toEqual( + 'filter-index-pattern-1' + ); + }); + + it('should list reference for current index pattern', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.references?.find( + (ref) => ref.name === 'indexpattern-datasource-current-indexpattern' + )?.id + ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f'); + }); + + it('should remove current index pattern id from datasource state', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.attributes.state.datasourceStates.indexpattern.currentIndexPatternId + ).toBeUndefined(); + }); + + it('should produce a valid document', () => { + const result = migrations['7.10.0'](example, context); + // changes to the outcome of this are critical - this test is a safe guard to not introduce changes accidentally + // if this test fails, make extra sure it's expected + expect(result).toMatchSnapshot(); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index d24a3e92cbd9c2..fdbfa1e455f60d 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -6,11 +6,16 @@ import { cloneDeep } from 'lodash'; import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { SavedObjectMigrationMap, SavedObjectMigrationFn } from 'src/core/server'; +import { + SavedObjectMigrationMap, + SavedObjectMigrationFn, + SavedObjectReference, + SavedObjectUnsanitizedDoc, +} from 'src/core/server'; +import { Query, Filter } from 'src/plugins/data/public'; +import { PersistableFilter } from '../common'; -interface LensDocShape { - id?: string; - type?: string; +interface LensDocShapePre710 { visualizationType: string | null; title: string; expression: string | null; @@ -21,18 +26,44 @@ interface LensDocShape { datasourceStates: { // This is hardcoded as our only datasource indexpattern: { + currentIndexPatternId: string; layers: Record< string, { columnOrder: string[]; columns: Record; + indexPatternId: string; } >; }; }; visualization: VisualizationState; - query: unknown; - filters: unknown[]; + query: Query; + filters: Filter[]; + }; +} + +interface LensDocShape { + id?: string; + type?: string; + visualizationType: string | null; + title: string; + state: { + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + layers: Record< + string, + { + columnOrder: string[]; + columns: Record; + } + >; + }; + }; + visualization: VisualizationState; + query: Query; + filters: PersistableFilter[]; }; } @@ -55,7 +86,10 @@ interface XYStatePost77 { * Removes the `lens_auto_date` subexpression from a stored expression * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} */ -const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { +const removeLensAutoDate: SavedObjectMigrationFn = ( + doc, + context +) => { const expression = doc.attributes.expression; if (!expression) { return doc; @@ -112,7 +146,10 @@ const removeLensAutoDate: SavedObjectMigrationFn = ( /** * Adds missing timeField arguments to esaggs in the Lens expression */ -const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { +const addTimeFieldToEsaggs: SavedObjectMigrationFn = ( + doc, + context +) => { const expression = doc.attributes.expression; if (!expression) { return doc; @@ -174,14 +211,14 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn = }; const removeInvalidAccessors: SavedObjectMigrationFn< - LensDocShape, - LensDocShape + LensDocShapePre710, + LensDocShapePre710 > = (doc) => { const newDoc = cloneDeep(doc); if (newDoc.attributes.visualizationType === 'lnsXY') { const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {}; const xyState = newDoc.attributes.state.visualization; - (newDoc.attributes as LensDocShape< + (newDoc.attributes as LensDocShapePre710< XYStatePost77 >).state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => { const layerId = layer.layerId; @@ -197,9 +234,86 @@ const removeInvalidAccessors: SavedObjectMigrationFn< return newDoc; }; +const extractReferences: SavedObjectMigrationFn = ({ + attributes, + references, + ...docMeta +}) => { + const savedObjectReferences: SavedObjectReference[] = []; + // add currently selected index pattern to reference list + savedObjectReferences.push({ + type: 'index-pattern', + id: attributes.state.datasourceStates.indexpattern.currentIndexPatternId, + name: 'indexpattern-datasource-current-indexpattern', + }); + + // add layer index patterns to list and remove index pattern ids from layers + const persistableLayers: Record< + string, + Omit< + LensDocShapePre710['state']['datasourceStates']['indexpattern']['layers'][string], + 'indexPatternId' + > + > = {}; + Object.entries(attributes.state.datasourceStates.indexpattern.layers).forEach( + ([layerId, { indexPatternId, ...persistableLayer }]) => { + savedObjectReferences.push({ + type: 'index-pattern', + id: indexPatternId, + name: `indexpattern-datasource-layer-${layerId}`, + }); + persistableLayers[layerId] = persistableLayer; + } + ); + + // add filter index patterns to reference list and remove index pattern ids from filter definitions + const persistableFilters = attributes.state.filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `filter-index-pattern-${i}`; + savedObjectReferences.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }); + + // put together new saved object format + const newDoc: SavedObjectUnsanitizedDoc = { + ...docMeta, + references: savedObjectReferences, + attributes: { + visualizationType: attributes.visualizationType, + title: attributes.title, + state: { + datasourceStates: { + indexpattern: { + layers: persistableLayers, + }, + }, + visualization: attributes.state.visualization, + query: attributes.state.query, + filters: persistableFilters, + }, + }, + }; + + return newDoc; +}; + export const migrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). '7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context), + '7.10.0': extractReferences, }; From 3031e668060ad56124a0f191f1c03cea71532697 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 21 Aug 2020 09:16:17 -0700 Subject: [PATCH 16/77] Update datasets UI copy to data streams (#75618) --- x-pack/plugins/ingest_manager/dev_docs/definitions.md | 6 +++--- .../ingest_manager/constants/page_paths.ts | 4 ++-- .../ingest_manager/hooks/use_breadcrumbs.tsx | 2 +- .../applications/ingest_manager/layouts/default.tsx | 2 +- .../sections/data_stream/list_page/index.tsx | 10 +++++----- .../standalone_instructions.tsx | 2 +- .../overview/components/datastream_section.tsx | 8 ++++---- .../public/applications/ingest_manager/types/index.ts | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md index be5aeb923e903b..d9ff597c5e84b6 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/definitions.md +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -13,9 +13,9 @@ definitions for one or multiple inputs and each input can contain one or multipl With the example of the nginx Package policy, it contains two inputs: `logs` and `nginx/metrics`. Logs and metrics are collected differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. -## Data Stream +## Data stream -Data Streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify +Data streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify ingesting data and the setup of Elasticsearch. ## Elastic Agent @@ -35,7 +35,7 @@ Fleet is the part of the Ingest Manager UI in Kibana that handles the part of en Ingest Management + Elastic Agent follow a strict new indexing strategy: `{type}-{dataset}-{namespace}`. An example for this is `logs-nginx.access-default`. More details about it can be found in the Index Strategy below. All data of -the index strategy is sent to Data Streams. +the index strategy is sent to data streams. ## Input diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts index 4a8dcfedc09361..c370f46b9f5a02 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -53,7 +53,7 @@ export const PAGE_ROUTING_PATHS = { fleet_agent_details_events: '/fleet/agents/:agentId', fleet_agent_details_details: '/fleet/agents/:agentId/details', fleet_enrollment_tokens: '/fleet/enrollment-tokens', - data_streams: '/datasets', + data_streams: '/data-streams', }; export const pagePathGetters: { @@ -80,5 +80,5 @@ export const pagePathGetters: { fleet_agent_details: ({ agentId, tabId }) => `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', - data_streams: () => '/datasets', + data_streams: () => '/data-streams', }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index 6ef1351dc5b603..1d80495d2b3471 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -207,7 +207,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { - defaultMessage: 'Datasets', + defaultMessage: 'Data streams', }), }, ], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 726da7a790b970..30294779d1a3d3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -96,7 +96,7 @@ export const DefaultLayout: React.FunctionComponent = ({ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index d8ab46fbf87f7f..53fd6a713b5231 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -32,7 +32,7 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => (

@@ -173,7 +173,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {

} @@ -216,14 +216,14 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { isLoading ? ( ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( emptyPrompt ) : ( ) } @@ -253,7 +253,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { placeholder: i18n.translate( 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', { - defaultMessage: 'Filter datasets', + defaultMessage: 'Filter data streams', } ), incremental: true, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx index abe834e7db19ce..049ceca82b309b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -157,7 +157,7 @@ export const StandaloneInstructions: React.FunctionComponent = ({ agentPo diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx index 41c011de2da5c2..bece6ec074b88e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx @@ -47,14 +47,14 @@ export const OverviewDatastreamSection: React.FC = () => { @@ -65,7 +65,7 @@ export const OverviewDatastreamSection: React.FC = () => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 80e27b7c4d0bf3..30a6742af6ea68 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -43,7 +43,7 @@ export { CreatePackagePolicyResponse, UpdatePackagePolicyRequest, UpdatePackagePolicyResponse, - // API schemas - Data Streams + // API schemas - Data streams GetDataStreamsResponse, // API schemas - Agents GetAgentsResponse, From 82e30f6effdecbc8d9c6a7d4a5f53fb267b60541 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 21 Aug 2020 10:48:04 -0600 Subject: [PATCH 17/77] Upgrade EUI to v27.4.1 (#75240) * eui to 27.4.1 * src snapshot updates * x-pack snapshot updates * remove increased default timeout * revert date change * delete default_timeout file * reinstate storyshot Co-authored-by: Elastic Machine --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- .../collapsible_nav.test.tsx.snap | 7748 +++++------------ .../header/__snapshots__/header.test.tsx.snap | 1281 ++- .../flyout_service.test.tsx.snap | 4 +- .../__snapshots__/modal_service.test.tsx.snap | 6 +- src/dev/jest/config.js | 1 - src/dev/jest/setup/default_timeout.js | 25 - .../__snapshots__/new_vis_modal.test.tsx.snap | 5080 +++-------- .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_sample_panel_action/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- x-pack/dev-tools/jest/create_jest_config.js | 1 - x-pack/package.json | 2 +- .../TransactionActionMenu.test.tsx.snap | 5 +- .../asset_manager.stories.storyshot | 1052 +-- .../custom_element_modal.stories.storyshot | 1888 ++-- .../keyboard_shortcuts_doc.stories.storyshot | 2207 +++-- .../saved_elements_modal.stories.storyshot | 1362 ++- .../__snapshots__/settings.test.tsx.snap | 898 +- .../upload_license.test.tsx.snap | 989 +-- .../report_info_button.test.tsx.snap | 1236 +-- .../helpers/home.helpers.ts | 2 +- .../__snapshots__/ml_flyout.test.tsx.snap | 261 +- yarn.lock | 8 +- 25 files changed, 7978 insertions(+), 16088 deletions(-) delete mode 100644 src/dev/jest/setup/default_timeout.js diff --git a/package.json b/package.json index 23fc31f369b8de..46418e52d85484 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.0-rc.2", "@elastic/ems-client": "7.9.3", - "@elastic/eui": "27.4.0", + "@elastic/eui": "27.4.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index a37281cb2263ff..531513481b1d4c 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "19.8.1", - "@elastic/eui": "27.4.0", + "@elastic/eui": "27.4.1", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 72d62730fa6980..2cfe232bf5653c 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -378,9 +378,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + +
+
+ +
+ +
+
- +
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - returnFocus={[Function]} - shards={Array []} - sideCar={ - Object { - "assignMedium": [Function], - "assignSyncMedium": [Function], - "options": Object { - "async": true, - "ssr": false, - }, - "read": [Function], - "useMedium": [Function], - } - } + + + + + +
+
+ +
+ + + + + - - -
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - returnFocus={[Function]} - shards={Array []} +

- - + Recently viewed +

+
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - returnFocus={[Function]} - shards={Array []} - /> - - - - + + + + + +
+ -
-
+
+ + + + + + +
+
+ +
+ + + + + + + -
- - + Kibana + +
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-kibana" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
+
+ +
+
+ +
+
-
- -
-
- -
-
+ visualize + + + + + +
  • -
    - - - -
    -
  • -
    -
    -
    + dashboard + + + + + +
    - - - +
    + +
    +
    + + + + + + + + + +

    + Observability +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-observability" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + -
    -
    + +
    + + +
    + - -
    -
    -
    - - - -
    -
    -
    -
    -
    + Observability + +
    -
    -
    - +
    + + + +
    +
    + +
    +
    +
    - - - - - - -

    - Observability -

    -
    -
    - + -
    -
    - -
    -
    + + + + - -
    -
    + -
    - - - -
    -
    -
    -
    -
    -
    -
    - - + + + + + +
    +
    +
    +
    +
    + + + + + + + + + + +

    + Security +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-security" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + -
    -
    + +
    + + +
    + - -
    -
    -
    - - - -
    -
    -
    -
    -
    + Security + +
    -
    -
    - + + + + + +
    + +
    +
    +
    - - - -

    - Management -

    -
    -
    - + -
    -
    - -
    -
    + + + + + +
    +
    +
    + +
    +
    + + + + + + +

    + Management +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-management" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    -
    -
    - - - -
    -
    - - + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + +
    +
    +
    + + +
    +
    + +
      +
      + - -
        - -
        - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
      • - -
      • - -
      -
      -
      -
    - - -
    - - - + , } } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" > - - - - + + + + +
    -
    -
    - - + + +
    +
    + + + + + + + @@ -5439,621 +2471,442 @@ exports[`CollapsibleNav renders the default nav 2`] = ` clickOutsideDisables={true} disabled={false} > - - -
    - - - +
    -
    - -
    -
    +
    + +
    + + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + -
    -
    - -
    -
    -
    - -
    - -
    -

    - No recently viewed items -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    + Recently viewed + +
    -
    -
    - -
    -
    - +
    + + + +
    +
    + +
    +
    +
    -
    - -
    -
    - -
      - -
      - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • - -
    -
    -
    +

    + No recently viewed items +

    -
    -
    -
    - - +
    + +
    +
    +
    + + + + + + +
    +
    + +
    + + +
    +
    + +
      - +
      + + Dock navigation + + , } } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" > - - - - + + + +
    +
    - -
    - - + + +
    + + + +
    - - +
    + + + + close + + + + + + + + +
    @@ -6299,581 +3152,442 @@ exports[`CollapsibleNav renders the default nav 3`] = ` clickOutsideDisables={true} disabled={true} > - - -
    - - - +
    -
    + - + + +
    +
    + +
    + + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    + - -
    - - - +

    + No recently viewed items +

    -
    -
    -
    - - +
    + +
    +
    +
    + +
    +
    + + + +
    +
    + +
    + + +
    +
    + +
      - - - -

      - Recently viewed -

      -
      -
      - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
      -
      - -
      -
      - -
      -
      -
      - -
      - -
      -

      - No recently viewed items -

      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      - -
      - , } - > - -
      -
      - -
        - -
        - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
      • - -
      • - -
      -
      -
      -
      -
      -
      -
      -
      - - - - - - + + + +
    +
    - -
    + + +
    + + + +
    - - + } + > + + +
    + + + + close + + + + + + + + +
    diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index a1920154d9f718..c02a763c401011 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -8999,766 +8999,635 @@ exports[`Header renders 3`] = ` clickOutsideDisables={true} disabled={true} > - - -
    - - - +
    + +
      + +
    • + +
    • +
      +
    +
    +
    +
    + +
    + + +
    +
    + +
    + +
    + } + >
    - -
    -
    +
    + +
    + + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    -
    - -
    -
    - - - -
    -
    -
    -
    - - + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    + +
      + - - - -

      - Recently viewed -

      -
      -
      - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" +
    • -
      -
      - +
    • +
      +
    +
    +
    +
    +
    + + +
    +
    + +
      + +
      - -
      - - - - -
      - -
      - -

      - Recently viewed -

      -
      -
      -
      -
      -
      + Undock navigation
      - -
      -
      - -
      -
      -
      - - - -
      -
      -
      -
      -
      -
      - - - -
      -
      - -
      - -
      -
      - -
        - -
      • - -
      • -
        -
      -
      -
      -
      -
      - , } - > - -
      -
      - -
        - -
        - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
      • - -
      • - -
      -
      -
      -
      -
      -
      -
      -
      - - - - - - + + +
      +
    +
    - -
    + + +
    + + + +
    - - + } + > + + +
    + + + + close + + + + + + + + +
    diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index fa83b34e06b813..a5c1d46f74709e 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -26,7 +26,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index fb00ddc38c6dca..aea52eb8e7ab7a 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -31,7 +31,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
    Modal content
    "`; +exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
    Modal content
    "`; exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ @@ -53,7 +53,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

    Some message

    "`; +exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

    Some message

    "`; exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ @@ -145,7 +145,7 @@ Array [ ] `; -exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
    Modal content
    "`; +exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
    Modal content
    "`; exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index d46b955f6668dd..74e1ec5e2b4edb 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -78,7 +78,6 @@ export default { setupFilesAfterEnv: [ '/src/dev/jest/setup/mocks.js', '/src/dev/jest/setup/react_testing_library.js', - '/src/dev/jest/setup/default_timeout.js', ], coverageDirectory: '/target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], diff --git a/src/dev/jest/setup/default_timeout.js b/src/dev/jest/setup/default_timeout.js deleted file mode 100644 index eea38e745b9601..00000000000000 --- a/src/dev/jest/setup/default_timeout.js +++ /dev/null @@ -1,25 +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. - */ - -/* eslint-env jest */ - -/** - * Set the default timeout for the unit tests to 30 seconds, temporarily - */ -jest.setTimeout(30 * 1000); diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index a27dfa13e743ed..6aed16e9377131 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -142,21 +142,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiOverlayMask euiOverlayMask--aboveHeader" >