From 3115ae650bb435395fc94e48a9a852a81abb2f7a Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 13 Jan 2020 19:33:34 +0000 Subject: [PATCH] [Lens][Dashboard] Adding Lens to Dashboard (#53110) (#54607) * First version of adding Lens to dashboard * Fix failing unit test * Replacing explicit Lens query param with a more generic one * Fixing failing unit test * Adding a unit test for redirect * Do not show Save New if adding from Dashboard * Adding functional test * Adding functional test * Fixing type issues * Renaming query params * Fixing failing unit test * Removing unused constants * Fixing erroneous imports * Fixing erroneous import * Fixing import * Fix failing typecheck * Removing timefilter from Dashboard URL * Fixing type error * Replacing time parsing with rison * Replacing URL regex parsing with legacy URLs * Fixing failing test Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../dashboard/__tests__/url_helper.test.ts | 117 ++++++++++++++++++ .../kibana/public/dashboard/legacy_imports.ts | 1 + .../np_ready/dashboard_app_controller.tsx | 14 +-- .../dashboard/np_ready/dashboard_constants.ts | 3 +- .../public/dashboard/np_ready/url_helper.ts | 102 +++++++++++++++ .../visualize/np_ready/editor/editor.js | 8 +- .../np_ready/wizard/new_vis_modal.test.tsx | 4 +- .../np_ready/wizard/new_vis_modal.tsx | 7 +- .../functional/page_objects/visualize_page.ts | 4 + .../lens/public/app_plugin/app.test.tsx | 25 ++++ .../plugins/lens/public/app_plugin/app.tsx | 18 ++- .../plugins/lens/public/app_plugin/plugin.tsx | 76 ++++++++++-- .../dashboard_mode/dashboard_empty_screen.js | 68 ++++++++++ .../functional/apps/dashboard_mode/index.js | 1 + 14 files changed, 420 insertions(+), 28 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts create mode 100644 x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts new file mode 100644 index 0000000000000..16773c02f5a7b --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('../', () => ({ + DashboardConstants: { + ADD_EMBEDDABLE_ID: 'addEmbeddableId', + ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + }, +})); + +jest.mock('../legacy_imports', () => { + return { + absoluteToParsedUrl: jest.fn(() => { + return { + basePath: '/pep', + appId: 'kibana', + appPath: '/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3', + hostname: 'localhost', + port: 5601, + protocol: 'http:', + addQueryParameter: () => {}, + getAbsoluteUrl: () => { + return 'http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3'; + }, + }; + }), + }; +}); + +import { + addEmbeddableToDashboardUrl, + getLensUrlFromDashboardAbsoluteUrl, + getUrlVars, +} from '../np_ready/url_helper'; + +describe('Dashboard URL Helper', () => { + beforeEach(() => { + jest.resetModules(); + }); + + it('addEmbeddableToDashboardUrl', () => { + const id = '123eb456cd'; + const type = 'lens'; + const urlVars = { + x: '1', + y: '2', + z: '3', + }; + const basePath = '/pep'; + const url = + "http://localhost:5601/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(addEmbeddableToDashboardUrl(url, basePath, id, urlVars, type)).toEqual( + `http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=${type}&addEmbeddableId=${id}&x=1&y=2&z=3` + ); + }); + + it('getUrlVars', () => { + let url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getUrlVars(url)).toEqual({ + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))', + _a: "(description:'',filters:!()", + }); + url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z'; + expect(getUrlVars(url)).toEqual({ + x: 'y', + y: 'z', + }); + url = 'http://notDashboardUrl'; + expect(getUrlVars(url)).toEqual({}); + url = 'http://localhost:5601/app/kibana#/dashboard/777182'; + expect(getUrlVars(url)).toEqual({}); + }); + + it('getLensUrlFromDashboardAbsoluteUrl', () => { + const id = '1244'; + const basePath = '/wev'; + let url = + "http://localhost:5601/wev/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( + 'http://localhost:5601/wev/app/kibana#/lens/edit/1244' + ); + + url = + "http://localhost:5601/wev/app/kibana#/dashboard/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"; + expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( + 'http://localhost:5601/wev/app/kibana#/lens/edit/1244' + ); + + url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182'; + expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( + 'http://myserver.mydomain.com:5601/wev/app/kibana#/lens/edit/1244' + ); + + url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getLensUrlFromDashboardAbsoluteUrl(url, '', id)).toEqual( + 'http://localhost:5601/app/kibana#/lens/edit/1244' + ); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index ec0913e5fb3e7..ba01919431080 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -67,3 +67,4 @@ export { IInjector } from 'ui/chrome'; export { SavedObjectLoader } from 'ui/saved_objects'; export { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize_embeddable'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; +export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 2523d1e60a741..2706b588a2ec4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -37,7 +37,6 @@ import { KbnUrl, SavedObjectSaveOpts, unhashUrl, - VISUALIZE_EMBEDDABLE_TYPE, } from '../legacy_imports'; import { FilterStateManager } from '../../../../data/public'; import { @@ -334,13 +333,12 @@ export class DashboardAppController { // This code needs to be replaced with a better mechanism for adding new embeddables of // any type from the add panel. Likely this will happen via creating a visualization "inline", // without navigating away from the UX. - if ($routeParams[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) { - container.addSavedObjectEmbeddable( - VISUALIZE_EMBEDDABLE_TYPE, - $routeParams[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] - ); - kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); - kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); + if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) { + const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]; + const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID]; + container.addSavedObjectEmbeddable(type, id); + kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_TYPE); + kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_ID); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts index b76b3f309874a..fe42e07912799 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts @@ -19,9 +19,10 @@ export const DashboardConstants = { ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard', - NEW_VISUALIZATION_ID_PARAM: 'addVisualization', LANDING_PAGE_PATH: '/dashboards', CREATE_NEW_DASHBOARD_URL: '/dashboard', + ADD_EMBEDDABLE_ID: 'addEmbeddableId', + ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', }; export function createDashboardEditUrl(id: string) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts new file mode 100644 index 0000000000000..ee9e3c4ef4781 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts @@ -0,0 +1,102 @@ +/* + * 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 { parse } from 'url'; +import { absoluteToParsedUrl } from '../legacy_imports'; +import { DashboardConstants } from './dashboard_constants'; +/** + * Return query params from URL + * @param url given url + */ +export function getUrlVars(url: string): Record { + const vars: Record = {}; + // @ts-ignore + url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(_, key, value) { + // @ts-ignore + vars[key] = value; + }); + return vars; +} + +/** * + * Returns dashboard URL with added embeddableType and embeddableId query params + * eg. + * input: url: http://localhost:5601/lib/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345, embeddableType: 'lens' + * output: http://localhost:5601/lib/app/kibana#dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + * @param url dasbhoard absolute url + * @param embeddableId id of the saved visualization + * @param basePath current base path + * @param urlVars url query params (optional) + * @param embeddableType 'lens' or 'visualization' (optional, default is 'lens') + */ +export function addEmbeddableToDashboardUrl( + url: string | undefined, + basePath: string, + embeddableId: string, + urlVars?: Record, + embeddableType?: string +): string | null { + if (!url) { + return null; + } + const dashboardUrl = getUrlWithoutQueryParams(url); + const dashboardParsedUrl = absoluteToParsedUrl(dashboardUrl, basePath); + if (urlVars) { + const keys = Object.keys(urlVars).sort(); + keys.forEach(key => { + dashboardParsedUrl.addQueryParameter(key, urlVars[key]); + }); + } + dashboardParsedUrl.addQueryParameter( + DashboardConstants.ADD_EMBEDDABLE_TYPE, + embeddableType || 'lens' + ); + dashboardParsedUrl.addQueryParameter(DashboardConstants.ADD_EMBEDDABLE_ID, embeddableId); + return dashboardParsedUrl.getAbsoluteUrl(); +} + +/** + * Return Lens URL from dashboard absolute URL + * @param dashboardAbsoluteUrl + * @param basePath current base path + * @param id Lens id + */ +export function getLensUrlFromDashboardAbsoluteUrl( + dashboardAbsoluteUrl: string | undefined | null, + basePath: string | null | undefined, + id: string +): string | null { + if (!dashboardAbsoluteUrl || basePath === null || basePath === undefined) { + return null; + } + const { host, protocol } = parse(dashboardAbsoluteUrl); + return `${protocol}//${host}${basePath}/app/kibana#/lens/edit/${id}`; +} + +/** + * Returns the portion of the URL without query params + * eg. + * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z + * output:http://localhost:5601/lib/app/kibana#/dashboard + * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z + * output: http://localhost:5601/lib/app/kibana#/dashboard/39292992 + * @param url url to parse + */ +function getUrlWithoutQueryParams(url: string): string { + return url.split('?')[0]; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index ed9bec9db4112..64653730473cd 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -35,8 +35,8 @@ import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; import { initVisEditorDirective } from './visualization_editor'; import { initVisualizationDirective } from './visualization'; - import { + VISUALIZE_EMBEDDABLE_TYPE, subscribeWithScope, absoluteToParsedUrl, KibanaParsedUrl, @@ -588,7 +588,11 @@ function VisualizeAppController( getBasePath() ); dashboardParsedUrl.addQueryParameter( - DashboardConstants.NEW_VISUALIZATION_ID_PARAM, + DashboardConstants.ADD_EMBEDDABLE_TYPE, + VISUALIZE_EMBEDDABLE_TYPE + ); + dashboardParsedUrl.addQueryParameter( + DashboardConstants.ADD_EMBEDDABLE_ID, savedVis.id ); kbnUrl.change(dashboardParsedUrl.appPath); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx index 2005133e6d03e..0ef1b711eafc8 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx @@ -144,7 +144,7 @@ describe('NewVisModal', () => { expect(window.location.assign).toBeCalledWith('#/visualize/create?type=vis&foo=true&bar=42'); }); - it('closes if visualization with aliasUrl and addToDashboard in editorParams', () => { + it('closes and redirects properly if visualization with aliasUrl and addToDashboard in editorParams', () => { const onClose = jest.fn(); window.location.assign = jest.fn(); const wrapper = mountWithIntl( @@ -160,7 +160,7 @@ describe('NewVisModal', () => { ); const visButton = wrapper.find('button[data-test-subj="visType-visWithAliasUrl"]'); visButton.simulate('click'); - expect(window.location.assign).toBeCalledWith('testbasepath/aliasUrl'); + expect(window.location.assign).toBeCalledWith('testbasepath/aliasUrl?addToDashboard'); expect(onClose).toHaveBeenCalled(); }); }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx index 9e8f46407f591..082fc3bc36b6b 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx @@ -143,15 +143,18 @@ class NewVisModal extends React.Component (await globalNav.getLastBreadcrumb()) === vizName ); } + + public async clickLensWidget() { + await this.clickVisType('lens'); + } } return new VisualizePage(); diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 1cdae05833b98..794128832461b 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -80,6 +80,7 @@ describe('Lens App', () => { docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; + addToDashboardMode?: boolean; }> { return ({ editorFrame: createMockFrame(), @@ -126,6 +127,7 @@ describe('Lens App', () => { docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; + addToDashboardMode?: boolean; }>; } @@ -306,6 +308,7 @@ describe('Lens App', () => { docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; + addToDashboardMode?: boolean; }>; beforeEach(() => { @@ -344,14 +347,19 @@ describe('Lens App', () => { async function save({ initialDocId, + addToDashboardMode, ...saveProps }: SaveProps & { initialDocId?: string; + addToDashboardMode?: boolean; }) { const args = { ...defaultArgs, docId: initialDocId, }; + if (addToDashboardMode) { + args.addToDashboardMode = addToDashboardMode; + } args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', @@ -543,6 +551,23 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(false); }); + + it('saves new doc and redirects to dashboard', async () => { + const { args } = await save({ + initialDocId: undefined, + addToDashboardMode: true, + newCopyOnSave: false, + newTitle: 'hello there', + }); + + expect(args.docStorage.save).toHaveBeenCalledWith({ + expression: 'kibana 3', + id: undefined, + title: 'hello there', + }); + + expect(args.redirectTo).toHaveBeenCalledWith('aaa'); + }); }); }); 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 cb57f2c884e38..f33cd41f46a11 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -13,6 +13,7 @@ import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_s import { AppMountContext, NotificationsStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { npStart } from 'ui/new_platform'; +import { FormattedMessage } from '@kbn/i18n/react'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; @@ -50,6 +51,7 @@ export function App({ docId, docStorage, redirectTo, + addToDashboardMode, }: { editorFrame: EditorFrameInstance; data: DataPublicPluginStart; @@ -58,6 +60,7 @@ export function App({ docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; + addToDashboardMode?: boolean; }) { const language = storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); @@ -166,6 +169,13 @@ export function App({ const { TopNavMenu } = npStart.plugins.navigation.ui; + const confirmButton = addToDashboardMode ? ( + + ) : null; + return ( { + .catch(e => { + // eslint-disable-next-line no-console + console.dir(e); trackUiEvent('save_failed'); core.notifications.toasts.addDanger( i18n.translate('xpack.lens.app.docSavingError', { @@ -337,10 +348,11 @@ export function App({ }} onClose={() => setState(s => ({ ...s, isSaveModalVisible: false }))} title={lastKnownDoc.title || ''} - showCopyOnSave={true} + showCopyOnSave={!addToDashboardMode} objectType={i18n.translate('xpack.lens.app.saveModalType', { defaultMessage: 'Lens visualization', })} + confirmButtonLabel={confirmButton} /> )} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index b1eac8e287bd8..7465de2dba7f1 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -14,11 +14,13 @@ import 'uiExports/visResponseHandlers'; import 'uiExports/savedObjectTypes'; import React from 'react'; -import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; -import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { render, unmountComponentAtNode } from 'react-dom'; import { CoreSetup, CoreStart, SavedObjectsClientContract } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import rison, { RisonObject, RisonValue } from 'rison-node'; +import { isObject } from 'lodash'; import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin'; @@ -41,6 +43,11 @@ import { import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../common'; import { KibanaLegacySetup } from '../../../../../../src/plugins/kibana_legacy/public'; import { EditorFrameStart } from '../types'; +import { + addEmbeddableToDashboardUrl, + getUrlVars, + getLensUrlFromDashboardAbsoluteUrl, +} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; export interface LensPluginSetupDependencies { kibana_legacy: KibanaLegacySetup; @@ -51,6 +58,9 @@ export interface LensPluginStartDependencies { dataShim: DataStart; } +export const isRisonObject = (value: RisonValue): value is RisonObject => { + return isObject(value); +}; export class AppPlugin { private startDependencies: { data: DataPublicPluginStart; @@ -84,7 +94,6 @@ export class AppPlugin { } const { data, savedObjectsClient, editorFrame } = this.startDependencies; addHelpMenuToAppChrome(context.core.chrome); - const instance = editorFrame.createInstance({}); setReportManager( @@ -93,9 +102,60 @@ export class AppPlugin { http: core.http, }) ); + const updateUrlTime = (urlVars: Record): void => { + const decoded: RisonObject = rison.decode(urlVars._g) as RisonObject; + if (!decoded) { + return; + } + // @ts-ignore + decoded.time = data.query.timefilter.timefilter.getTime(); + urlVars._g = rison.encode((decoded as unknown) as RisonObject); + }; + const redirectTo = ( + routeProps: RouteComponentProps<{ id?: string }>, + addToDashboardMode: boolean, + id?: string + ) => { + if (!id) { + routeProps.history.push('/lens'); + } else if (!addToDashboardMode) { + routeProps.history.push(`/lens/edit/${id}`); + } else if (addToDashboardMode && id) { + routeProps.history.push(`/lens/edit/${id}`); + const url = context.core.chrome.navLinks.get('kibana:dashboard'); + if (!url) { + throw new Error('Cannot get last dashboard url'); + } + const lastDashboardAbsoluteUrl = url.url; + const basePath = context.core.http.basePath.get(); + const lensUrl = getLensUrlFromDashboardAbsoluteUrl( + lastDashboardAbsoluteUrl, + basePath, + id + ); + if (!lastDashboardAbsoluteUrl || !lensUrl) { + throw new Error('Cannot get last dashboard url'); + } + window.history.pushState({}, '', lensUrl); + const urlVars = getUrlVars(lastDashboardAbsoluteUrl); + updateUrlTime(urlVars); // we need to pass in timerange in query params directly + const dashboardParsedUrl = addEmbeddableToDashboardUrl( + lastDashboardAbsoluteUrl, + basePath, + id, + urlVars + ); + if (!dashboardParsedUrl) { + throw new Error('Problem parsing dashboard url'); + } + window.history.pushState({}, '', dashboardParsedUrl); + } + }; const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { trackUiEvent('loaded'); + const addToDashboardMode = + !!routeProps.location.search && routeProps.location.search.includes('addToDashboard'); return ( { - if (!id) { - routeProps.history.push('/lens'); - } else { - routeProps.history.push(`/lens/edit/${id}`); - } - }} + redirectTo={id => redirectTo(routeProps, addToDashboardMode, id)} + addToDashboardMode={addToDashboardMode} /> ); }; @@ -119,6 +174,7 @@ export class AppPlugin { trackUiEvent('loaded_404'); return ; } + render( diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js new file mode 100644 index 0000000000000..c90a0ae6d19fc --- /dev/null +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -0,0 +1,68 @@ +/* + * 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 default function({ getPageObjects, getService }) { + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const dashboardVisualizations = getService('dashboardVisualizations'); + const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens']); + + describe('empty dashboard', function() { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('lens/basic'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + after(async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + async function createAndAddLens(title) { + log.debug(`createAndAddLens(${title})`); + const inViewMode = await PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await PageObjects.dashboard.switchToEditMode(); + } + await PageObjects.visualize.clickLensWidget(); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.configureDimension({ + dimension: + '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: + '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: + '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + operation: 'terms', + field: 'ip', + }); + await PageObjects.lens.save(title); + } + + it('adds Lens visualization to empty dashboard', async () => { + const title = 'Dashboard Test Lens'; + await testSubjects.exists('addVisualizationButton'); + await testSubjects.click('addVisualizationButton'); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await createAndAddLens(title); + await PageObjects.dashboard.waitForRenderComplete(); + await testSubjects.exists(`embeddablePanelHeading-${title}`); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard_mode/index.js b/x-pack/test/functional/apps/dashboard_mode/index.js index 2a98634ba40d5..09b9717ea9f02 100644 --- a/x-pack/test/functional/apps/dashboard_mode/index.js +++ b/x-pack/test/functional/apps/dashboard_mode/index.js @@ -9,5 +9,6 @@ export default function({ loadTestFile }) { this.tags('ciGroup7'); loadTestFile(require.resolve('./dashboard_view_mode')); + loadTestFile(require.resolve('./dashboard_empty_screen')); }); }