diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index 6fdcd5213b3a6..33cf892435708 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -8,6 +8,7 @@ import * as Joi from 'joi'; import { Server } from 'hapi'; import { resolve } from 'path'; import { LegacyPluginInitializer } from 'src/legacy/types'; +import mappings from './mappings.json'; import { PLUGIN_ID } from './common'; @@ -27,7 +28,22 @@ export const lens: LegacyPluginInitializer = kibana => { main: `plugins/${PLUGIN_ID}/index`, }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, + mappings, + savedObjectsManagement: { + lens: { + defaultSearchField: 'title', + isImportableAndExportable: true, + getTitle: (obj: { attributes: { title: string } }) => obj.attributes.title, + getInAppUrl: (obj: { id: string }) => ({ + path: `/app/lens#/edit/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'lens.show', + }), + }, + }, + // TODO: savedObjectsManagement is not in the uiExports type definition, + // so, we have to either fix the type signature and deal with merge + // conflicts, or simply cas to any here, and fix this later. + } as any, config: () => { return Joi.object({ diff --git a/x-pack/legacy/plugins/lens/mappings.json b/x-pack/legacy/plugins/lens/mappings.json new file mode 100644 index 0000000000000..4c860a7171829 --- /dev/null +++ b/x-pack/legacy/plugins/lens/mappings.json @@ -0,0 +1,18 @@ +{ + "lens": { + "properties": { + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + }, + "datasourceType": { + "type": "keyword" + }, + "state": { + "type": "text" + } + } + } +} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 0e469dbeb5634..c877ef4ce5292 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -6,14 +6,23 @@ import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; - +import { toastNotifications } from 'ui/notify'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; export function App({ editorFrame }: { editorFrame: EditorFrameInstance }) { return ( - + + toastNotifications.addDanger({ + title: e.message, + }), + }} + /> ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 8306e6476a967..4bcf4d8b5e42e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -42,6 +42,15 @@ describe('editor_frame', () => { let expressionRendererMock: ExpressionRenderer; + const defaultProps = { + store: { + save: jest.fn(), + load: jest.fn(), + }, + redirectTo: jest.fn(), + onError: jest.fn(), + }; + beforeEach(() => { mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); @@ -57,6 +66,7 @@ describe('editor_frame', () => { act(() => { mount( { act(() => { mount( { act(() => { mount( { act(() => { mount( { act(() => { mount( { act(() => { mount( { mount( initialState }, }} @@ -229,6 +245,7 @@ describe('editor_frame', () => { it('should render the resulting expression using the expression renderer', async () => { const instance = mount( 'vis' }, }} @@ -272,6 +289,7 @@ Object { it('should re-render config panel after state update', async () => { mount( { mount( { mount( { mount( { mount( { instance = mount( { mount( { mount( { const instance = mount( ; visualizationMap: Record; - + redirectTo: (path: string) => void; initialDatasourceId: string | null; initialVisualizationId: string | null; - ExpressionRenderer: ExpressionRenderer; + onError: (e: { message: string }) => void; } export function EditorFrame(props: EditorFrameProps) { const [state, dispatch] = useReducer(reducer, props, getInitialState); + const { onError } = props; // create public datasource api for current state // as soon as datasource is available and memoize it @@ -50,26 +58,46 @@ export function EditorFrame(props: EditorFrameProps) { ] ); + useEffect( + () => { + if (props.doc) { + dispatch({ + type: 'VISUALIZATION_LOADED', + doc: props.doc, + }); + } else { + dispatch({ + type: 'RESET', + state: getInitialState(props), + }); + } + }, + [props.doc] + ); + // Initialize current datasource useEffect( () => { let datasourceGotSwitched = false; if (state.datasource.isLoading && state.datasource.activeId) { - props.datasourceMap[state.datasource.activeId].initialize().then(datasourceState => { - if (!datasourceGotSwitched) { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - newState: datasourceState, - }); - } - }); + props.datasourceMap[state.datasource.activeId] + .initialize(props.doc && props.doc.state.datasource) + .then(datasourceState => { + if (!datasourceGotSwitched) { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + newState: datasourceState, + }); + } + }) + .catch(onError); return () => { datasourceGotSwitched = true; }; } }, - [state.datasource.activeId, state.datasource.isLoading] + [props.doc, state.datasource.activeId, state.datasource.isLoading] ); // Initialize visualization as soon as datasource is ready @@ -92,9 +120,41 @@ export function EditorFrame(props: EditorFrameProps) { [datasourcePublicAPI, state.visualization.activeId, state.visualization.state] ); - if (state.datasource.activeId && !state.datasource.isLoading) { + const datasource = + state.datasource.activeId && !state.datasource.isLoading + ? props.datasourceMap[state.datasource.activeId] + : undefined; + + const visualization = state.visualization.activeId + ? props.visualizationMap[state.visualization.activeId] + : undefined; + + if (datasource) { return ( + { + if (datasource && visualization) { + save({ + datasource, + dispatch, + visualization, + state, + redirectTo: props.redirectTo, + store: props.store, + }).catch(onError); + } + }} + disabled={state.saving || !state.datasource.activeId || !state.visualization.activeId} + > + {i18n.translate('xpack.lens.editorFrame.Save', { + defaultMessage: 'Save', + })} + + + } dataPanel={ } workspacePanel={ - + + + } suggestionsPanel={ - {props.dataPanel} - - {props.workspacePanel} - - - {props.configPanel} - {props.suggestionsPanel} - +
{props.navPanel}
+ +
+ {props.dataPanel} + + {props.workspacePanel} + + + {props.configPanel} + {props.suggestionsPanel} + +
); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss index d384c03195538..b57fe73adb8b2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss @@ -7,6 +7,16 @@ right: 0; bottom: 0; overflow: hidden; + flex-direction: column; +} + +.lnsHeader { + padding: $euiSize; + padding-bottom: 0; +} + +.lnsPageMainContent { + display: flex; } .lnsSidebar { @@ -73,4 +83,14 @@ overflow-x: hidden; } +.lnsTitleInput { + width: 100%; + min-width: 100%; + border: 0; + font: inherit; + background: transparent; + box-shadow: none; + font-size: 1.2em; +} + @import './suggestion_panel.scss'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts new file mode 100644 index 0000000000000..ef4eed2b63a5d --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -0,0 +1,177 @@ +/* + * 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 { save, Props } from './save'; +import { Action } from './state_management'; + +describe('save editor frame state', () => { + const saveArgs: Props = { + dispatch: jest.fn(), + redirectTo: jest.fn(), + datasource: { getPersistableState: x => x }, + visualization: { getPersistableState: x => x }, + state: { + title: 'aaa', + datasource: { activeId: '1', isLoading: false, state: {} }, + saving: false, + visualization: { activeId: '2', state: {} }, + }, + store: { + async save() { + return { id: 'foo' }; + }, + }, + }; + + it('dispatches saved status actions before and after saving', async () => { + let saved = false; + + const dispatch = jest.fn((action: Action) => { + if ( + (action.type === 'SAVING' && action.isSaving && saved) || + (action.type === 'SAVING' && !action.isSaving && !saved) + ) { + throw new Error('Saving status was incorrectly set'); + } + }); + + await save({ + ...saveArgs, + dispatch, + state: { + title: 'aaa', + datasource: { activeId: '1', isLoading: false, state: {} }, + saving: false, + visualization: { activeId: '2', state: {} }, + }, + store: { + async save() { + saved = true; + return { id: 'foo' }; + }, + }, + }); + + expect(dispatch).toHaveBeenCalledWith({ type: 'SAVING', isSaving: true }); + expect(dispatch).toHaveBeenCalledWith({ type: 'SAVING', isSaving: false }); + }); + + it('allows saves if an error occurs', async () => { + const dispatch = jest.fn(); + + await expect( + save({ + ...saveArgs, + dispatch, + state: { + title: 'aaa', + datasource: { activeId: '1', isLoading: false, state: {} }, + saving: false, + visualization: { activeId: '2', state: {} }, + }, + store: { + async save() { + throw new Error('aw shnap!'); + }, + }, + }) + ).rejects.toThrow(); + + expect(dispatch).toHaveBeenCalledWith({ type: 'SAVING', isSaving: true }); + expect(dispatch).toHaveBeenCalledWith({ type: 'SAVING', isSaving: false }); + }); + + it('transforms from internal state to persisted doc format', async () => { + const store = { + save: jest.fn(async () => ({ id: 'bar' })), + }; + await save({ + ...saveArgs, + store, + datasource: { + getPersistableState(state) { + return { + stuff: `${state}_datsource_persisted`, + }; + }, + }, + state: { + title: 'bbb', + datasource: { activeId: '1', isLoading: false, state: '2' }, + saving: false, + visualization: { activeId: '3', state: '4' }, + }, + visualization: { + getPersistableState(state) { + return { + things: `${state}_vis_persisted`, + }; + }, + }, + }); + + expect(store.save).toHaveBeenCalledWith({ + datasourceType: '1', + id: undefined, + state: { + datasource: { stuff: '2_datsource_persisted' }, + visualization: { things: '4_vis_persisted' }, + }, + title: 'bbb', + type: 'lens', + visualizationType: '3', + }); + }); + + it('redirects to the edit screen if the id changes', async () => { + const redirectTo = jest.fn(); + const dispatch = jest.fn(); + await save({ + ...saveArgs, + dispatch, + redirectTo, + state: { + title: 'ccc', + datasource: { activeId: '1', isLoading: false, state: {} }, + saving: false, + visualization: { activeId: '2', state: {} }, + }, + store: { + async save() { + return { id: 'bazinga' }; + }, + }, + }); + + expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_PERSISTED_ID', id: 'bazinga' }); + expect(redirectTo).toHaveBeenCalledWith('/edit/bazinga'); + }); + + it('does not redirect to the edit screen if the id does not change', async () => { + const redirectTo = jest.fn(); + const dispatch = jest.fn(); + await save({ + ...saveArgs, + dispatch, + redirectTo, + state: { + title: 'ddd', + datasource: { activeId: '1', isLoading: false, state: {} }, + persistedId: 'foo', + saving: false, + visualization: { activeId: '2', state: {} }, + }, + store: { + async save() { + return { id: 'foo' }; + }, + }, + }); + + expect(dispatch.mock.calls.some(({ type }) => type === 'UPDATE_PERSISTED_ID')).toBeFalsy(); + expect(redirectTo).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts new file mode 100644 index 0000000000000..472220e83a44e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action, EditorFrameState } from './state_management'; +import { Document } from '../../persistence/saved_object_store'; + +export interface Props { + datasource: { getPersistableState: (state: unknown) => unknown }; + dispatch: (value: Action) => void; + redirectTo: (path: string) => void; + state: EditorFrameState; + store: { save: (doc: Document) => Promise<{ id: string }> }; + visualization: { getPersistableState: (state: unknown) => unknown }; +} + +export async function save({ + datasource, + dispatch, + redirectTo, + state, + store, + visualization, +}: Props) { + try { + dispatch({ type: 'SAVING', isSaving: true }); + + const doc = await store.save({ + id: state.persistedId, + title: state.title, + type: 'lens', + visualizationType: state.visualization.activeId, + datasourceType: state.datasource.activeId, + state: { + datasource: datasource.getPersistableState(state.datasource.state), + visualization: visualization.getPersistableState(state.visualization.state), + }, + }); + + if (doc.id !== state.persistedId) { + dispatch({ type: 'UPDATE_PERSISTED_ID', id: doc.id }); + redirectTo(`/edit/${doc.id}`); + } + } finally { + dispatch({ type: 'SAVING', isSaving: false }); + } +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts index 5b767d2d05582..5f1861269dc0e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts @@ -15,6 +15,12 @@ describe('editor_frame state management', () => { beforeEach(() => { props = { + onError: jest.fn(), + redirectTo: jest.fn(), + store: { + load: jest.fn(), + save: jest.fn(), + }, datasourceMap: { testDatasource: ({} as unknown) as Datasource }, visualizationMap: { testVis: ({ initialize: jest.fn() } as unknown) as Visualization }, initialDatasourceId: 'testDatasource', @@ -56,6 +62,8 @@ describe('editor_frame state management', () => { state: {}, isLoading: false, }, + saving: false, + title: 'aaa', visualization: { activeId: 'testVis', state: {}, @@ -79,6 +87,8 @@ describe('editor_frame state management', () => { state: {}, isLoading: false, }, + saving: false, + title: 'bbb', visualization: { activeId: 'testVis', state: {}, @@ -103,6 +113,8 @@ describe('editor_frame state management', () => { state: {}, isLoading: false, }, + saving: false, + title: 'ccc', visualization: { activeId: 'testVis', state: testVisState, @@ -129,6 +141,8 @@ describe('editor_frame state management', () => { state: {}, isLoading: false, }, + saving: false, + title: 'ddd', visualization: { activeId: 'testVis', state: testVisState, @@ -154,6 +168,8 @@ describe('editor_frame state management', () => { state: {}, isLoading: false, }, + saving: false, + title: 'eee', visualization: { activeId: 'testVis', state: {}, @@ -170,5 +186,177 @@ describe('editor_frame state management', () => { expect(newState.datasource.activeId).toBe('testDatasource2'); expect(newState.datasource.state).toBe(null); }); + + it('should mark as saving', () => { + const newState = reducer( + { + datasource: { + activeId: 'a', + state: {}, + isLoading: false, + }, + saving: false, + title: 'fff', + visualization: { + activeId: 'b', + state: {}, + }, + }, + { + type: 'SAVING', + isSaving: true, + } + ); + + expect(newState.saving).toBeTruthy(); + }); + + it('should mark as saved', () => { + const newState = reducer( + { + datasource: { + activeId: 'a', + state: {}, + isLoading: false, + }, + saving: false, + title: 'hhh', + visualization: { + activeId: 'b', + state: {}, + }, + }, + { + type: 'SAVING', + isSaving: false, + } + ); + + expect(newState.saving).toBeFalsy(); + }); + + it('should change the persisted id', () => { + const newState = reducer( + { + datasource: { + activeId: 'a', + state: {}, + isLoading: false, + }, + saving: false, + title: 'iii', + visualization: { + activeId: 'b', + state: {}, + }, + }, + { + type: 'UPDATE_PERSISTED_ID', + id: 'baz', + } + ); + + expect(newState.persistedId).toEqual('baz'); + }); + + it('should reset the state', () => { + const newState = reducer( + { + datasource: { + activeId: 'a', + state: {}, + isLoading: false, + }, + saving: false, + title: 'jjj', + visualization: { + activeId: 'b', + state: {}, + }, + }, + { + type: 'RESET', + state: { + datasource: { + activeId: 'z', + isLoading: false, + state: { hola: 'muchacho' }, + }, + persistedId: 'bar', + saving: false, + title: 'lll', + visualization: { + activeId: 'q', + state: { my: 'viz' }, + }, + }, + } + ); + + expect(newState).toMatchObject({ + datasource: { + activeId: 'z', + isLoading: false, + state: { hola: 'muchacho' }, + }, + persistedId: 'bar', + saving: false, + visualization: { + activeId: 'q', + state: { my: 'viz' }, + }, + }); + }); + + it('should load the state from the doc', () => { + const newState = reducer( + { + datasource: { + activeId: 'a', + state: {}, + isLoading: false, + }, + saving: false, + title: 'mmm', + visualization: { + activeId: 'b', + state: {}, + }, + }, + { + type: 'VISUALIZATION_LOADED', + doc: { + datasourceType: 'a', + id: 'b', + state: { + datasource: { foo: 'c' }, + visualization: { bar: 'd' }, + }, + title: 'heyo!', + type: 'lens', + visualizationType: 'line', + }, + } + ); + + expect(newState).toEqual({ + datasource: { + activeId: 'a', + isLoading: true, + state: { + foo: 'c', + }, + }, + persistedId: 'b', + saving: false, + title: 'heyo!', + visualization: { + activeId: 'line', + state: { + bar: 'd', + }, + }, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index 5d999337038a1..df56544e5c134 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -4,9 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { EditorFrameProps } from '../editor_frame'; +import { Document } from '../../persistence/saved_object_store'; export interface EditorFrameState { + persistedId?: string; + saving: boolean; + title: string; visualization: { activeId: string | null; state: unknown; @@ -19,6 +24,22 @@ export interface EditorFrameState { } export type Action = + | { + type: 'RESET'; + state: EditorFrameState; + } + | { + type: 'SAVING'; + isSaving: boolean; + } + | { + type: 'UPDATE_TITLE'; + title: string; + } + | { + type: 'UPDATE_PERSISTED_ID'; + id: string; + } | { type: 'UPDATE_DATASOURCE_STATE'; newState: unknown; @@ -27,6 +48,10 @@ export type Action = type: 'UPDATE_VISUALIZATION_STATE'; newState: unknown; } + | { + type: 'VISUALIZATION_LOADED'; + doc: Document; + } | { type: 'SWITCH_VISUALIZATION'; newVisualizationId: string; @@ -40,6 +65,8 @@ export type Action = export const getInitialState = (props: EditorFrameProps): EditorFrameState => { return { + saving: false, + title: i18n.translate('xpack.lens.chartTitle', { defaultMessage: 'New visualization' }), datasource: { state: null, isLoading: Boolean(props.initialDatasourceId), @@ -54,6 +81,31 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { export const reducer = (state: EditorFrameState, action: Action): EditorFrameState => { switch (action.type) { + case 'SAVING': + return { ...state, saving: action.isSaving }; + case 'RESET': + return action.state; + case 'UPDATE_PERSISTED_ID': + return { ...state, persistedId: action.id }; + case 'UPDATE_TITLE': + return { ...state, title: action.title }; + case 'VISUALIZATION_LOADED': + return { + ...state, + persistedId: action.doc.id, + title: action.doc.title, + datasource: { + ...state.datasource, + activeId: action.doc.datasourceType || null, + isLoading: true, + state: action.doc.state.datasource, + }, + visualization: { + ...state.visualization, + activeId: action.doc.visualizationType, + state: action.doc.state.visualization, + }, + }; case 'SWITCH_DATASOURCE': return { ...state, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 7a0017d83ad3a..796fce7ddbcf5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -6,16 +6,7 @@ import React, { useState, useEffect, useMemo, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCodeBlock, - EuiSpacer, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiTitle, - EuiPageContentBody, -} from '@elastic/eui'; - +import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { toExpression } from '@kbn/interpreter/common'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; import { Action } from './state_management'; @@ -157,19 +148,8 @@ export function WorkspacePanel({ } return ( - - - - -

New Visualization

-
-
-
- - - {renderVisualization()} - - -
+ + {renderVisualization()} + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx new file mode 100644 index 0000000000000..e878b870b2760 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiPageContent, EuiPageContentHeader, EuiPageContentBody } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Action } from './state_management'; + +interface Props { + title: string; + dispatch: React.Dispatch; + children: React.ReactNode | React.ReactNode[]; +} + +export function WorkspacePanelWrapper({ children, title, dispatch }: Props) { + return ( + + + dispatch({ type: 'UPDATE_TITLE', title: e.target.value })} + aria-label={i18n.translate('xpack.lens.chartTitleAriaLabel', { + defaultMessage: 'Title', + })} + /> + + {children} + + ); +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/initializable_component.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/initializable_component.test.tsx new file mode 100644 index 0000000000000..3f935d91054b8 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/initializable_component.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, mount } from 'enzyme'; +import { InitializableComponent } from './initializable_component'; + +function resolvable() { + let resolve: (value: {}) => void; + + return { + promise: new Promise(res => (resolve = res)), + resolve: (x: {}) => resolve(x), + }; +} + +describe('InitializableComponent', () => { + test('renders nothing if loading', () => { + const component = render( + Promise.resolve({ hello: 'world' })} + render={props =>
{props!.hello}
} + /> + ); + + expect(component).toMatchInlineSnapshot(`null`); + }); + + test('passes the resolved props to render', async () => { + const initPromise = Promise.resolve({ test: 'props' }); + const mockRender = jest.fn(() =>
); + + mount( initPromise} render={mockRender} />); + + await initPromise; + expect(mockRender).toHaveBeenCalledWith({ test: 'props' }); + }); + + test('allows an undefined resolve', async () => { + const initPromise = Promise.resolve(); + const mockRender = jest.fn(() =>
); + + mount( initPromise} render={mockRender} />); + + await initPromise; + expect(mockRender).toHaveBeenCalledWith(undefined); + }); + + test('ignores stale promise results', async () => { + const firstInit = resolvable(); + const secondInit = resolvable(); + const mockRender = jest.fn(() =>
); + + const component = mount( + firstInit.promise} render={mockRender} /> + ); + + component.setProps({ + watch: ['b'], + init: () => secondInit.promise, + render: mockRender, + }); + + firstInit.resolve({ hello: 1 }); + secondInit.resolve({ hello: 2 }); + await secondInit.promise; + + expect(mockRender).not.toHaveBeenCalledWith({ hello: 1 }); + expect(mockRender).toHaveBeenCalledWith({ hello: 2 }); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/initializable_component.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/initializable_component.tsx new file mode 100644 index 0000000000000..a5da8adb7e485 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/initializable_component.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; + +interface Props { + watch: unknown[]; + init: () => Promise; + render: (props: T) => JSX.Element | null; +} + +export function InitializableComponent(props: Props) { + const [state, setState] = useState<{ isLoading: boolean; result?: T }>({ + isLoading: true, + result: undefined, + }); + + useEffect(() => { + let isStale = false; + + props.init().then(result => { + if (!isStale) { + setState({ isLoading: false, result }); + } + }); + + return () => { + isStale = true; + }; + }, props.watch); + + if (state.isLoading) { + // TODO: Handle the loading / undefined result case + return null; + } + + return props.render(state.result!); +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index c34d8bc85d578..ae169aa67148e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -4,21 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EditorFramePlugin } from './plugin'; +import React from 'react'; +import { EditorFramePlugin, init, InitializedEditor } from './plugin'; import { createMockDependencies, MockedDependencies, createMockDatasource, createMockVisualization, } from './mocks'; +import { SavedObjectStore, Document } from '../persistence'; +import { shallow, mount } from 'enzyme'; -// calling this function will wait for all pending Promises from mock -// datasources to be processed by its callers. -const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); +jest.mock('ui/chrome', () => ({ + getSavedObjectsClient: jest.fn(), +})); // mock away actual data plugin to prevent all of it being loaded jest.mock('../../../../../../src/legacy/core_plugins/data/public/setup', () => {}); +function mockStore(): SavedObjectStore { + return { + load: jest.fn(), + save: jest.fn(), + }; +} + describe('editor_frame plugin', () => { let pluginInstance: EditorFramePlugin; let mountpoint: Element; @@ -38,62 +48,178 @@ describe('editor_frame plugin', () => { expect(() => { const publicAPI = pluginInstance.setup(null, pluginDependencies); const instance = publicAPI.createInstance({}); - instance.mount(mountpoint); + instance.mount(mountpoint, { onError: jest.fn() }); instance.unmount(); }).not.toThrowError(); }); - it('should render something in the provided dom element', () => { - const publicAPI = pluginInstance.setup(null, pluginDependencies); - const instance = publicAPI.createInstance({}); - instance.mount(mountpoint); - - expect(mountpoint.hasChildNodes()).toBe(true); - - instance.unmount(); - }); - it('should not have child nodes after unmount', () => { const publicAPI = pluginInstance.setup(null, pluginDependencies); const instance = publicAPI.createInstance({}); - instance.mount(mountpoint); + instance.mount(mountpoint, { onError: jest.fn() }); instance.unmount(); expect(mountpoint.hasChildNodes()).toBe(false); }); - it('should initialize and render provided datasource', async () => { - const mockDatasource = createMockDatasource(); - const publicAPI = pluginInstance.setup(null, pluginDependencies); - publicAPI.registerDatasource('test', mockDatasource); - - const instance = publicAPI.createInstance({}); - instance.mount(mountpoint); - - await waitForPromises(); - - expect(mockDatasource.initialize).toHaveBeenCalled(); - expect(mockDatasource.renderDataPanel).toHaveBeenCalled(); - - instance.unmount(); + describe('init', () => { + it('should do nothing if the persistedId is undefined', async () => { + const store = mockStore(); + expect( + await init({ + store, + onError: jest.fn(), + }) + ).toEqual({}); + expect(store.load).not.toHaveBeenCalled(); + }); + + it('should load the document, if persistedId is defined', async () => { + const doc: Document = { + datasourceType: 'indexpattern', + id: 'hoi', + state: { datasource: 'foo', visualization: 'bar' }, + title: 'shazm', + visualizationType: 'fanci', + type: 'lens', + }; + + const store = { + ...mockStore(), + load: jest.fn(async () => doc), + }; + + expect( + await init({ + persistedId: 'hoi', + store, + onError: jest.fn(), + }) + ).toEqual({ doc }); + + expect(store.load).toHaveBeenCalledWith('hoi'); + }); + + it('should call onError if an error occurs while loading', async () => { + const error = new Error('dang!'); + const store = { + ...mockStore(), + load: jest.fn(async () => { + throw error; + }), + }; + const onError = jest.fn(); + + expect( + await init({ + persistedId: 'hoi', + store, + onError, + }) + ).toEqual({ error }); + + expect(onError).toHaveBeenCalledWith(error); + }); + + it('should not call onError if a 404 error occurs while loading', async () => { + const error = new Object({ statusCode: 404 }); + const store = { + ...mockStore(), + load: jest.fn(async () => { + throw error; + }), + }; + const onError = jest.fn(); + + expect( + await init({ + persistedId: 'hoi', + store, + onError, + }) + ).toEqual({ error }); + + expect(onError).not.toHaveBeenCalled(); + }); }); - it('should initialize visualization and render config panel', async () => { - const mockDatasource = createMockDatasource(); - const mockVisualization = createMockVisualization(); - const publicAPI = pluginInstance.setup(null, pluginDependencies); - - publicAPI.registerDatasource('test', mockDatasource); - publicAPI.registerVisualization('test', mockVisualization); - - const instance = publicAPI.createInstance({}); - instance.mount(mountpoint); - - await waitForPromises(); - - expect(mockVisualization.initialize).toHaveBeenCalled(); - expect(mockVisualization.renderConfigPanel).toHaveBeenCalled(); - - instance.unmount(); + describe('render', () => { + it('renders 404 if given a 404 error', () => { + const error = { statusCode: 404, message: 'Ruh roh!' }; + const result = shallow( +
} + routeProps={{ history: { push: jest.fn() } }} + store={mockStore()} + onError={jest.fn()} + /> + ); + expect(result).toMatchInlineSnapshot(``); + }); + + it('redirects via route history', () => { + const historyPush = jest.fn(); + const component = mount( +
} + routeProps={{ history: { push: historyPush } }} + store={mockStore()} + onError={jest.fn()} + /> + ); + + const redirectTo = component.find('[data-test-subj="lnsEditorFrame"]').prop('redirectTo') as ( + path: string + ) => void; + redirectTo('mehnewurl'); + expect(historyPush).toHaveBeenCalledWith('mehnewurl'); + }); + + it('uses the document datasource and visualization types, if available', () => { + const component = mount( +
} + routeProps={{ history: { push: jest.fn() } }} + store={mockStore()} + onError={jest.fn()} + /> + ); + + const frame = component.find('[data-test-subj="lnsEditorFrame"]'); + + expect(frame.prop('initialDatasourceId')).toEqual('b'); + expect(frame.prop('initialVisualizationId')).toEqual('d'); + }); + + it('uses the first datasource and visualization type, if there is no document', () => { + const component = mount( +
} + routeProps={{ history: { push: jest.fn() } }} + store={mockStore()} + onError={jest.fn()} + /> + ); + + const frame = component.find('[data-test-subj="lnsEditorFrame"]'); + + expect(frame.prop('initialDatasourceId')).toEqual('a'); + expect(frame.prop('initialVisualizationId')).toEqual('c'); + }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 4112a928342b5..b55a84d32c17a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -8,28 +8,60 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup } from 'src/core/public'; +import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; +import chrome from 'ui/chrome'; import { DataSetup, ExpressionRenderer, } from '../../../../../../src/legacy/core_plugins/data/public'; import { data } from '../../../../../../src/legacy/core_plugins/data/public/setup'; -import { Datasource, Visualization, EditorFrameSetup, EditorFrameInstance } from '../types'; +import { + Datasource, + Visualization, + EditorFrameSetup, + EditorFrameInstance, + ErrorCallback, +} from '../types'; import { EditorFrame } from './editor_frame'; +import { SavedObjectIndexStore, SavedObjectStore, Document } from '../persistence'; +import { InitializableComponent } from './initializable_component'; export interface EditorFrameSetupPlugins { data: DataSetup; } +interface InitializationResult { + doc?: Document; + error?: { message: string }; +} + +interface InitializationProps { + persistedId?: string; + store: SavedObjectStore; + onError: ErrorCallback; +} + +interface RenderProps extends InitializationResult { + routeProps: { history: { push: (path: string) => void } }; + store: SavedObjectStore; + onError: ErrorCallback; + datasources: Record; + visualizations: Record; + expressionRenderer: ExpressionRenderer; +} + export class EditorFramePlugin { constructor() {} - private ExpressionRenderer: ExpressionRenderer | null = null; + private ExpressionRenderer: ExpressionRenderer | null = null; private readonly datasources: Record = {}; private readonly visualizations: Record = {}; private createInstance(): EditorFrameInstance { let domElement: Element; + const store = new SavedObjectIndexStore(chrome.getSavedObjectsClient()); + function unmount() { if (domElement) { unmountComponentAtNode(domElement); @@ -37,22 +69,41 @@ export class EditorFramePlugin { } return { - mount: element => { - unmount(); + mount: (element, { onError }) => { domElement = element; - const firstDatasourceId = Object.keys(this.datasources)[0]; - const firstVisualizationId = Object.keys(this.visualizations)[0]; + const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { + const persistedId = routeProps.match.params.id; + + return ( + init({ persistedId, store, onError })} + render={({ doc, error }) => ( + + )} + /> + ); + }; render( - + + + + + + + , domElement ); @@ -85,4 +136,66 @@ export const editorFrameSetup = () => editorFrame.setup(null, { data, }); + export const editorFrameStop = () => editorFrame.stop(); + +function NotFound() { + return

TODO: 404 Page

; +} + +function is404(error?: unknown) { + return error && (error as { statusCode: number }).statusCode === 404; +} + +export async function init({ + persistedId, + store, + onError, +}: InitializationProps): Promise { + if (!persistedId) { + return {}; + } else { + return store + .load(persistedId) + .then(doc => ({ doc })) + .catch((error: Error) => { + if (!is404(error)) { + onError(error); + } + return { error }; + }); + } +} + +export function InitializedEditor({ + doc, + error, + routeProps, + onError, + store, + datasources, + visualizations, + expressionRenderer, +}: RenderProps) { + const firstDatasourceId = Object.keys(datasources)[0]; + const firstVisualizationId = Object.keys(visualizations)[0]; + + if (is404(error)) { + return ; + } + + return ( + routeProps.history.push(path)} + doc={doc} + /> + ); +} diff --git a/x-pack/legacy/plugins/lens/public/persistence/index.ts b/x-pack/legacy/plugins/lens/public/persistence/index.ts new file mode 100644 index 0000000000000..1f823ff75c8c6 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/persistence/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 './saved_object_store'; diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts new file mode 100644 index 0000000000000..53d2a0cc08ad1 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { SavedObjectIndexStore } from './saved_object_store'; + +describe('LensStore', () => { + function testStore(testId?: string) { + const client = { + create: jest.fn(() => Promise.resolve({ id: testId || 'testid' })), + update: jest.fn((_type: string, id: string) => Promise.resolve({ id })), + get: jest.fn(), + }; + + return { + client, + store: new SavedObjectIndexStore(client), + }; + } + + describe('save', () => { + test('creates and returns a visualization document', async () => { + const { client, store } = testStore('FOO'); + const doc = await store.save({ + title: 'Hello', + visualizationType: 'bar', + datasourceType: 'indexpattern', + state: { + datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, + visualization: { x: 'foo', y: 'baz' }, + }, + }); + + expect(doc).toEqual({ + id: 'FOO', + title: 'Hello', + visualizationType: 'bar', + datasourceType: 'indexpattern', + state: { + datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, + visualization: { x: 'foo', y: 'baz' }, + }, + }); + + expect(client.create).toHaveBeenCalledTimes(1); + expect(client.create).toHaveBeenCalledWith('lens', { + datasourceType: 'indexpattern', + title: 'Hello', + visualizationType: 'bar', + state: JSON.stringify({ + datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, + visualization: { x: 'foo', y: 'baz' }, + }), + }); + }); + + test('updates and returns a visualization document', async () => { + const { client, store } = testStore(); + const doc = await store.save({ + id: 'Gandalf', + title: 'Even the very wise cannot see all ends.', + visualizationType: 'line', + datasourceType: 'indexpattern', + state: { + datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + visualization: { gear: ['staff', 'pointy hat'] }, + }, + }); + + expect(doc).toEqual({ + id: 'Gandalf', + title: 'Even the very wise cannot see all ends.', + visualizationType: 'line', + datasourceType: 'indexpattern', + state: { + datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + visualization: { gear: ['staff', 'pointy hat'] }, + }, + }); + + expect(client.update).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledWith('lens', 'Gandalf', { + title: 'Even the very wise cannot see all ends.', + visualizationType: 'line', + datasourceType: 'indexpattern', + state: JSON.stringify({ + datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + visualization: { gear: ['staff', 'pointy hat'] }, + }), + }); + }); + }); + + describe('load', () => { + test('parses the visState', async () => { + const { client, store } = testStore(); + client.get = jest.fn(async () => ({ + id: 'Paul', + type: 'lens', + attributes: { + title: 'Hope clouds observation.', + visualizationType: 'dune', + state: '{ "datasource": { "giantWorms": true } }', + }, + })); + const doc = await store.load('Paul'); + + expect(doc).toEqual({ + id: 'Paul', + type: 'lens', + title: 'Hope clouds observation.', + visualizationType: 'dune', + state: { + datasource: { giantWorms: true }, + }, + }); + + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.get).toHaveBeenCalledWith('lens', 'Paul'); + }); + + test('throws if an error is returned', async () => { + const { client, store } = testStore(); + client.get = jest.fn(async () => ({ + id: 'Paul', + type: 'lens', + attributes: { + title: 'Hope clouds observation.', + visualizationType: 'dune', + state: '{ "datasource": { "giantWorms": true } }', + }, + error: new Error('shoot dang!'), + })); + + await expect(store.load('Paul')).rejects.toThrow('shoot dang!'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts new file mode 100644 index 0000000000000..930ee36ea3729 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -0,0 +1,84 @@ +/* + * 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 { SavedObjectAttributes } from 'target/types/server'; + +export interface Document { + id?: string; + type?: string; + visualizationType: string | null; + datasourceType: string | null; + title: string; + state: { + datasource: unknown; + visualization: unknown; + }; +} + +const DOC_TYPE = 'lens'; + +interface SavedObjectClient { + create: (type: string, object: SavedObjectAttributes) => Promise<{ id: string }>; + update: (type: string, id: string, object: SavedObjectAttributes) => Promise<{ id: string }>; + get: ( + type: string, + id: string + ) => Promise<{ + id: string; + type: string; + attributes: SavedObjectAttributes; + error?: { message: string }; + }>; +} + +export interface DocumentSaver { + save: (vis: Document) => Promise<{ id: string }>; +} + +export interface DocumentLoader { + load: (id: string) => Promise; +} + +export type SavedObjectStore = DocumentLoader & DocumentSaver; + +export class SavedObjectIndexStore implements SavedObjectStore { + private client: SavedObjectClient; + + constructor(client: SavedObjectClient) { + this.client = client; + } + + async save(vis: Document) { + const { id, type, ...rest } = vis; + const attributes = { + ...rest, + state: JSON.stringify(rest.state), + }; + const result = await (id + ? this.client.update(DOC_TYPE, id, attributes) + : this.client.create(DOC_TYPE, attributes)); + + return { + ...vis, + id: result.id, + }; + } + + async load(id: string): Promise { + const { type, attributes, error } = await this.client.get(DOC_TYPE, id); + + if (error) { + throw error; + } + + return { + ...attributes, + id, + type, + state: JSON.parse(((attributes as unknown) as { state: string }).state as string), + } as Document; + } +} diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 6be550a2342cd..c532d2d69b0a8 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -10,8 +10,10 @@ import { DragContextState } from './drag_drop'; // eslint-disable-next-line export interface EditorFrameOptions {} +export type ErrorCallback = (e: { message: string }) => void; + export interface EditorFrameInstance { - mount: (element: Element) => void; + mount: (element: Element, props: { onError: ErrorCallback }) => void; unmount: () => void; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap index 2cf89a4a58196..a2aebeff9c3b6 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap @@ -29,9 +29,6 @@ Object { ], "splitSeriesAccessors": Array [], "stackAccessors": Array [], - "title": Array [ - "Foo", - ], "x": Array [ Object { "chain": Array [ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index 33750937cebe8..62fcda60c9950 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -31,7 +31,6 @@ export const buildExpression = ( function: 'lens_xy_chart', arguments: { seriesType: [state.seriesType], - title: [state.title], legend: [ { type: 'expression', diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index 57a5bcd4966c8..069adaedd95f9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -150,7 +150,6 @@ export type SeriesType = 'bar' | 'horizontal_bar' | 'line' | 'area'; export interface XYArgs { seriesType: SeriesType; - title: string; legend: LegendConfig; y: YConfig; x: XConfig; @@ -160,7 +159,6 @@ export interface XYArgs { export interface XYState { seriesType: SeriesType; - title: string; legend: LegendConfig; y: YState; x: XConfig; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index 95c4543f32547..df051298f2c20 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -34,7 +34,6 @@ describe('XYConfigPanel', () => { seriesType: 'bar', splitSeriesAccessors: [], stackAccessors: [], - title: 'Test Chart', x: { accessor: 'foo', position: Position.Bottom, @@ -125,32 +124,6 @@ describe('XYConfigPanel', () => { }); }); - test('allows editing the chart title', () => { - const testSetTitle = (title: string) => { - const setState = jest.fn(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_title').onChange as Function)({ target: { value: title } }); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(testSetTitle('Hoi')).toMatchObject({ - title: 'Hoi', - }); - expect(testSetTitle('There!')).toMatchObject({ - title: 'There!', - }); - }); - test('allows changing legend position', () => { const testLegendPosition = (position: Position) => { const setState = jest.fn(); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 86883b4e629e3..b570525b10dcc 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -105,24 +105,6 @@ export function XYConfigPanel(props: VisualizationProps) { /> - - setState({ ...state, title: e.target.value })} - aria-label={i18n.translate('xpack.lens.xyChart.chartTitleAriaLabel', { - defaultMessage: 'Title', - })} - /> - -