From 813a896f51415bec6ea5c3f8ba866c558b6eabac Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Thu, 12 Mar 2020 23:57:05 -0500 Subject: [PATCH 01/32] partial progress on async loading / searching of dashboard titles --- .../dashboard_drilldown_config.tsx | 24 ++++++++---- .../dashboard_drilldowns_services.ts | 5 ++- .../collect_config.tsx | 39 ++++++++++++++----- .../drilldown.tsx | 4 +- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx index b45ba602b9bb1..5e800ed04a4c7 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch, EuiComboBox } from '@elastic/eui'; import { txtChooseDestinationDashboard } from './i18n'; export interface DashboardItem { @@ -21,6 +21,8 @@ export interface DashboardDrilldownConfigProps { onDashboardSelect: (dashboardId: string) => void; onCurrentFiltersToggle?: () => void; onKeepRangeToggle?: () => void; + onSearchChange: (searchString: string) => void; + isLoading: boolean; } export const DashboardDrilldownConfig: React.FC = ({ @@ -31,17 +33,25 @@ export const DashboardDrilldownConfig: React.FC = onDashboardSelect, onCurrentFiltersToggle, onKeepRangeToggle, + onSearchChange, + isLoading, }) => { // TODO: use i18n below. + // todo - don't assume selectedTitle is in set + const selectedTitle = dashboards.find(item => item.id === activeDashboardId)?.title || ''; return ( <> - ({ value: id, text: title }))} - value={activeDashboardId} - onChange={e => onDashboardSelect(e.target.value)} + + async + selectedOptions={ + activeDashboardId ? [{ label: selectedTitle, value: activeDashboardId }] : [] + } + options={dashboards.map(({ id, title }) => ({ label: title, value: id }))} + onChange={([{ value = '' } = { value: '' }]) => onDashboardSelect(value)} + onSearchChange={onSearchChange} + isLoading={isLoading} + singleSelection={{ asPlainText: true }} /> {!!onCurrentFiltersToggle && ( diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index ed0cb425ee106..b936522fe6be2 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -34,7 +34,8 @@ export class DashboardDrilldownsService { ) { const overlays = async () => (await core.getStartServices())[0].overlays; const drilldowns = async () => (await core.getStartServices())[1].drilldowns; - const savedObjects = async () => (await core.getStartServices())[0].savedObjects.client; + const getSavedObjectsClient = async () => + (await core.getStartServices())[0].savedObjects.client; const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays, drilldowns }); plugins.uiActions.registerAction(actionFlyoutCreateDrilldown); @@ -45,7 +46,7 @@ export class DashboardDrilldownsService { plugins.uiActions.attachAction(CONTEXT_MENU_DRILLDOWNS_TRIGGER, actionFlyoutEditDrilldown); const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({ - savedObjects, + getSavedObjectsClient, }); plugins.drilldowns.registerDrilldown(dashboardToDashboardDrilldown); } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx index 778a6b3ef6b31..9d42662734d1a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx @@ -8,6 +8,7 @@ import React, { useState, useEffect } from 'react'; import { CollectConfigProps } from './types'; import { DashboardDrilldownConfig } from '../../../components/dashboard_drilldown_config'; import { Params } from './drilldown'; +import { SimpleSavedObject } from '../../../../../../../src/core/public'; export interface CollectConfigContainerProps extends CollectConfigProps { params: Params; @@ -16,18 +17,36 @@ export interface CollectConfigContainerProps extends CollectConfigProps { export const CollectConfigContainer: React.FC = ({ config, onConfig, - params: { savedObjects }, + params: { getSavedObjectsClient }, }) => { - const [dashboards] = useState([ - { id: 'dashboard1', title: 'Dashboard 1' }, - { id: 'dashboard2', title: 'Dashboard 2' }, - { id: 'dashboard3', title: 'Dashboard 3' }, - { id: 'dashboard4', title: 'Dashboard 4' }, - ]); + const [dashboards, setDashboards] = useState([]); + const [searchString, setSearchString] = useState(); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - // TODO: Load dashboards... - }, [savedObjects]); + setIsLoading(true); + getSavedObjectsClient().then(savedObjectsClient => { + savedObjectsClient + .find({ + type: 'dashboard', + search: searchString ? `${searchString}*` : undefined, + // todo search by id + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + perPage: 100, + }) + .then(({ savedObjects }) => { + const dashboardList = savedObjects.map( + (savedObject: SimpleSavedObject<{ id: string; title: string }>) => ({ + id: savedObject.id, + title: savedObject.attributes.title, + }) + ); + setDashboards(dashboardList); + setIsLoading(false); + }); + }); + }, [getSavedObjectsClient, searchString]); return ( = ({ dashboards={dashboards} currentFilters={config.useCurrentDashboardFilters} keepRange={config.useCurrentDashboardDataRange} + isLoading={isLoading} onDashboardSelect={dashboardId => { onConfig({ ...config, dashboardId }); }} + onSearchChange={setSearchString} onCurrentFiltersToggle={() => onConfig({ ...config, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index c839ef8ee04ef..f991ef7d25c13 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -19,7 +19,7 @@ export const dashboards = [ ]; export interface Params { - savedObjects: () => Promise; + getSavedObjectsClient: () => Promise; } export class DashboardToDashboardDrilldown @@ -43,7 +43,7 @@ export class DashboardToDashboardDrilldown public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); public readonly createConfig = () => ({ - dashboardId: '123', + dashboardId: '', useCurrentDashboardDataRange: true, useCurrentDashboardFilters: true, }); From 65ff148de0bc01e7c7850a3357f01c313acf2332 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 13 Mar 2020 09:01:38 +0100 Subject: [PATCH 02/32] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20make=20combobox=20?= =?UTF-8?q?full=20width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard_drilldown_config/dashboard_drilldown_config.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx index 5e800ed04a4c7..3dc9c5626ab3b 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx @@ -41,7 +41,7 @@ export const DashboardDrilldownConfig: React.FC = const selectedTitle = dashboards.find(item => item.id === activeDashboardId)?.title || ''; return ( <> - + async selectedOptions={ @@ -52,6 +52,7 @@ export const DashboardDrilldownConfig: React.FC = onSearchChange={onSearchChange} isLoading={isLoading} singleSelection={{ asPlainText: true }} + fullWidth /> {!!onCurrentFiltersToggle && ( From d46fcf067962d68ebfebcb19d9b6bd005e123aeb Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Mon, 16 Mar 2020 17:28:45 -0500 Subject: [PATCH 03/32] filtering combobox polish --- .../dashboard_drilldown_config.story.tsx | 6 + .../dashboard_drilldown_config.tsx | 26 ++- .../dashboard_drilldown_config/i18n.ts | 14 ++ .../collect_config.tsx | 148 ++++++++++++------ 4 files changed, 131 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx index 8e204b044a136..de6897e56104d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx @@ -30,6 +30,8 @@ const InteractiveDemo: React.FC = () => { onDashboardSelect={id => setActiveDashboardId(id)} onCurrentFiltersToggle={() => setCurrentFilters(old => !old)} onKeepRangeToggle={() => setKeepRange(old => !old)} + onSearchChange={() => {}} + isLoading={false} /> ); }; @@ -40,6 +42,8 @@ storiesOf('components/DashboardDrilldownConfig', module) activeDashboardId={'dashboard2'} dashboards={dashboards} onDashboardSelect={e => console.log('onDashboardSelect', e)} + onSearchChange={() => {}} + isLoading={false} /> )) .add('with switches', () => ( @@ -49,6 +53,8 @@ storiesOf('components/DashboardDrilldownConfig', module) onDashboardSelect={e => console.log('onDashboardSelect', e)} onCurrentFiltersToggle={() => console.log('onCurrentFiltersToggle')} onKeepRangeToggle={() => console.log('onKeepRangeToggle')} + onSearchChange={() => {}} + isLoading={false} /> )) .add('interactive demo', () => ); diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx index 5e800ed04a4c7..a9c0a610c3b40 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx @@ -5,17 +5,16 @@ */ import React from 'react'; -import { EuiFormRow, EuiSwitch, EuiComboBox } from '@elastic/eui'; -import { txtChooseDestinationDashboard } from './i18n'; - -export interface DashboardItem { - id: string; - title: string; -} +import { EuiFormRow, EuiSwitch, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { + txtChooseDestinationDashboard, + txtUseCurrentFilters, + txtUseCurrentDateRange, +} from './i18n'; export interface DashboardDrilldownConfigProps { activeDashboardId?: string; - dashboards: DashboardItem[]; + dashboards: Array>; currentFilters?: boolean; keepRange?: boolean; onDashboardSelect: (dashboardId: string) => void; @@ -36,9 +35,8 @@ export const DashboardDrilldownConfig: React.FC = onSearchChange, isLoading, }) => { - // TODO: use i18n below. - // todo - don't assume selectedTitle is in set - const selectedTitle = dashboards.find(item => item.id === activeDashboardId)?.title || ''; + const selectedTitle = dashboards.find(item => item.value === activeDashboardId)?.label || ''; + return ( <> @@ -47,7 +45,7 @@ export const DashboardDrilldownConfig: React.FC = selectedOptions={ activeDashboardId ? [{ label: selectedTitle, value: activeDashboardId }] : [] } - options={dashboards.map(({ id, title }) => ({ label: title, value: id }))} + options={dashboards} onChange={([{ value = '' } = { value: '' }]) => onDashboardSelect(value)} onSearchChange={onSearchChange} isLoading={isLoading} @@ -58,7 +56,7 @@ export const DashboardDrilldownConfig: React.FC = @@ -68,7 +66,7 @@ export const DashboardDrilldownConfig: React.FC = diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/i18n.ts b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/i18n.ts index 38fe6dd150853..6f2c29693fcb0 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/i18n.ts +++ b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/i18n.ts @@ -12,3 +12,17 @@ export const txtChooseDestinationDashboard = i18n.translate( defaultMessage: 'Choose destination dashboard', } ); + +export const txtUseCurrentFilters = i18n.translate( + 'xpack.dashboard.components.DashboardDrilldownConfig.useCurrentFilters', + { + defaultMessage: "Use current dashboard's filters", + } +); + +export const txtUseCurrentDateRange = i18n.translate( + 'xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange', + { + defaultMessage: "Use current dashboard's date range", + } +); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx index 9d42662734d1a..cbced9024a8d3 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx @@ -4,73 +4,123 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { debounce, findIndex } from 'lodash'; import { CollectConfigProps } from './types'; import { DashboardDrilldownConfig } from '../../../components/dashboard_drilldown_config'; import { Params } from './drilldown'; import { SimpleSavedObject } from '../../../../../../../src/core/public'; +const mergeDashboards = ( + dashboards: Array>, + selectedDashboard?: EuiComboBoxOptionOption +) => { + // if we have a selected dashboard and its not in the list, append it + if (selectedDashboard && findIndex(dashboards, { value: selectedDashboard.value }) === -1) { + return [selectedDashboard, ...dashboards]; + } + return dashboards; +}; + +const dashboardSavedObjectToMenuItem = ( + savedObject: SimpleSavedObject<{ + title: string; + }> +) => ({ + value: savedObject.id, + label: savedObject.attributes.title, +}); + export interface CollectConfigContainerProps extends CollectConfigProps { params: Params; } -export const CollectConfigContainer: React.FC = ({ - config, - onConfig, - params: { getSavedObjectsClient }, -}) => { - const [dashboards, setDashboards] = useState([]); - const [searchString, setSearchString] = useState(); - const [isLoading, setIsLoading] = useState(false); +interface CollectConfigContainerState { + dashboards: Array>; + searchString?: string; + isLoading: boolean; + selectedDashboard?: EuiComboBoxOptionOption; + delay: boolean; +} + +export class CollectConfigContainer extends React.Component< + CollectConfigContainerProps, + CollectConfigContainerState +> { + state = { + dashboards: [], + isLoading: false, + searchString: undefined, + selectedDashboard: undefined, + delay: false, + }; + + componentDidMount() { + this.loadSelectedDashboard(); + this.loadDashboards(); + } - useEffect(() => { - setIsLoading(true); - getSavedObjectsClient().then(savedObjectsClient => { + loadSelectedDashboard() { + const { config } = this.props; + this.props.params.getSavedObjectsClient().then(savedObjectsClient => { + if (config.dashboardId) { + savedObjectsClient + .get<{ title: string }>('dashboard', config.dashboardId) + .then(dashboard => { + this.setState({ selectedDashboard: dashboardSavedObjectToMenuItem(dashboard) }); + }); + } + }); + } + + loadDashboards(searchString?: string) { + this.setState({ searchString, isLoading: true }); + this.props.params.getSavedObjectsClient().then(savedObjectsClient => { savedObjectsClient - .find({ + .find<{ title: string }>({ type: 'dashboard', search: searchString ? `${searchString}*` : undefined, - // todo search by id searchFields: ['title^3', 'description'], defaultSearchOperator: 'AND', perPage: 100, }) .then(({ savedObjects }) => { - const dashboardList = savedObjects.map( - (savedObject: SimpleSavedObject<{ id: string; title: string }>) => ({ - id: savedObject.id, - title: savedObject.attributes.title, - }) - ); - setDashboards(dashboardList); - setIsLoading(false); + if (searchString === this.state.searchString) { + const dashboardList = savedObjects.map(dashboardSavedObjectToMenuItem); + this.setState({ dashboards: dashboardList, isLoading: false }); + } }); }); - }, [getSavedObjectsClient, searchString]); + } - return ( - { - onConfig({ ...config, dashboardId }); - }} - onSearchChange={setSearchString} - onCurrentFiltersToggle={() => - onConfig({ - ...config, - useCurrentDashboardFilters: !config.useCurrentDashboardFilters, - }) - } - onKeepRangeToggle={() => - onConfig({ - ...config, - useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange, - }) - } - /> - ); -}; + render() { + const { config, onConfig } = this.props; + const { dashboards, selectedDashboard, isLoading } = this.state; + return ( + { + onConfig({ ...config, dashboardId }); + }} + onSearchChange={debounce(() => this.loadDashboards(), 500)} + onCurrentFiltersToggle={() => + onConfig({ + ...config, + useCurrentDashboardFilters: !config.useCurrentDashboardFilters, + }) + } + onKeepRangeToggle={() => + onConfig({ + ...config, + useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange, + }) + } + /> + ); + } +} From 699d853b1e338eb2a641f02bb95dbf64d6eef51b Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Mon, 16 Mar 2020 18:59:29 -0500 Subject: [PATCH 04/32] storybook fix --- .../dashboard_drilldown_config.story.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx index de6897e56104d..0978af654e9c4 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx @@ -11,9 +11,9 @@ import { storiesOf } from '@storybook/react'; import { DashboardDrilldownConfig } from '.'; export const dashboards = [ - { id: 'dashboard1', title: 'Dashboard 1' }, - { id: 'dashboard2', title: 'Dashboard 2' }, - { id: 'dashboard3', title: 'Dashboard 3' }, + { value: 'dashboard1', label: 'Dashboard 1' }, + { value: 'dashboard2', label: 'Dashboard 2' }, + { value: 'dashboard3', label: 'Dashboard 3' }, ]; const InteractiveDemo: React.FC = () => { From d3ac7ad32b15051e41d55cc1d3ffff54a6f0d64a Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Mon, 16 Mar 2020 23:54:13 -0500 Subject: [PATCH 05/32] implement navigating to dashboard, seems like a type problem --- .../dashboard_to_dashboard_drilldown/drilldown.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index f991ef7d25c13..bc2341f2c8dac 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -53,7 +53,12 @@ export class DashboardToDashboardDrilldown return true; }; - public readonly execute = () => { - alert('Go to another dashboard!'); + // it seems like tthtis fn is being execute with the wrong arguments + // first param should be Config but its { config: Config; name: string; actionFactory: string; } ( I thtink ) + + // @ts-ignore + public readonly execute = async ({ config }: Config, context: ActionContext) => { + console.log('context', context); // eslint-disable-line + window.location.hash = `#/dashboard/${config.dashboardId}`; }; } From 43693993b02401d6c9e4a53d2e4b07032756ba44 Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Tue, 17 Mar 2020 00:45:32 -0500 Subject: [PATCH 06/32] try navToApp --- .../dashboard_to_dashboard_drilldown/drilldown.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 5ba27423c5b8e..422d2d74ea99b 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -15,6 +15,7 @@ import { txtGoToDashboard } from './i18n'; export interface Params { getSavedObjectsClient: () => Promise; + getNavigateToApp: () => Promise; } export class DashboardToDashboardDrilldown @@ -48,12 +49,15 @@ export class DashboardToDashboardDrilldown return true; }; - // it seems like tthtis fn is being execute with the wrong arguments + // it seems like this fn is being execute with the wrong arguments // first param should be Config but its { config: Config; name: string; actionFactory: string; } ( I thtink ) // @ts-ignore public readonly execute = async ({ config }: Config, context: ActionContext) => { - console.log('context', context); // eslint-disable-line - window.location.hash = `#/dashboard/${config.dashboardId}`; + // todo - need to complete this + await this.params.getNavigateToApp().then(navigateToApp => { + navigateToApp('kibana', { path: `#/dashboard/${config.dashboardId}` }); + }); + // window.location.hash = `#/dashboard/${config.dashboardId}`; }; } From a276b964d07f6a5ab2065b92ec5fc107d5597083 Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Tue, 17 Mar 2020 17:39:14 -0500 Subject: [PATCH 07/32] filter out current dashboard --- .../collect_config.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx index cbced9024a8d3..2880f8dc9f902 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx @@ -11,6 +11,7 @@ import { CollectConfigProps } from './types'; import { DashboardDrilldownConfig } from '../../../components/dashboard_drilldown_config'; import { Params } from './drilldown'; import { SimpleSavedObject } from '../../../../../../../src/core/public'; +import { IEmbeddable } from '../../../../../../../src/plugins/embeddable/public'; const mergeDashboards = ( dashboards: Array>, @@ -34,6 +35,12 @@ const dashboardSavedObjectToMenuItem = ( export interface CollectConfigContainerProps extends CollectConfigProps { params: Params; + context: { + place: string; + placeContext: { + embeddable: IEmbeddable; + }; + }; } interface CollectConfigContainerState { @@ -75,6 +82,8 @@ export class CollectConfigContainer extends React.Component< } loadDashboards(searchString?: string) { + const currentDashboard = this.props.context.placeContext.embeddable.parent; + const currentDashboardId = currentDashboard && currentDashboard.id; this.setState({ searchString, isLoading: true }); this.props.params.getSavedObjectsClient().then(savedObjectsClient => { savedObjectsClient @@ -87,7 +96,9 @@ export class CollectConfigContainer extends React.Component< }) .then(({ savedObjects }) => { if (searchString === this.state.searchString) { - const dashboardList = savedObjects.map(dashboardSavedObjectToMenuItem); + const dashboardList = savedObjects + .map(dashboardSavedObjectToMenuItem) + .filter(({ value }) => value !== currentDashboardId); this.setState({ dashboards: dashboardList, isLoading: false }); } }); @@ -97,6 +108,7 @@ export class CollectConfigContainer extends React.Component< render() { const { config, onConfig } = this.props; const { dashboards, selectedDashboard, isLoading } = this.state; + return ( Date: Wed, 18 Mar 2020 01:13:13 -0500 Subject: [PATCH 08/32] rough draft linking to a dashboard --- .../public/np_ready/public/index.ts | 1 + src/plugins/dashboard/public/url_generator.ts | 4 +-- .../public/lib/triggers/triggers.ts | 6 ++-- x-pack/plugins/dashboard_enhanced/kibana.json | 2 +- .../dashboard_enhanced/public/plugin.ts | 2 ++ .../dashboard_drilldowns_services.ts | 8 +++++ .../collect_config.tsx | 14 ++++----- .../drilldown.tsx | 30 ++++++++++++------- .../dashboard_to_dashboard_drilldown/types.ts | 5 ++-- 9 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index b59eb2277411c..92c5af1413545 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -41,6 +41,7 @@ export { VisTypeAlias, VisType } from './vis_types'; export { VisSavedObject } from './types'; export { Vis, VisParams, VisState } from './vis'; import { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable'; +export { VisualizeEmbeddable }; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; export { TypesService } from './vis_types/types_service'; diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 5f1255bc9d45f..174493eaad50e 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -61,7 +61,7 @@ export const createDirectAccessDashboardLinkGenerator = ( createUrl: async state => { const startServices = await getStartServices(); const useHash = state.useHash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; + // const appBasePath = startServices.appBasePath; const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`; const appStateUrl = setStateToKbnUrl( @@ -71,7 +71,7 @@ export const createDirectAccessDashboardLinkGenerator = ( filters: state.filters, }, { useHash }, - `${appBasePath}#/${hash}` + `kibana#/${hash}` // use appBasePath once dashboards is migrated, using 'kibana' for now ); return setStateToKbnUrl( diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 0052403816eb8..651b64eddd58b 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -24,8 +24,10 @@ export interface EmbeddableContext { embeddable: IEmbeddable; } -export interface EmbeddableVisTriggerContext { - embeddable: IEmbeddable; +// I think this could benefit using a type variable for IEmbeddable +// but I'm concerned that the change will touch many places +export interface EmbeddableVisTriggerContext { + embeddable: T; timeFieldName: string; data: { e: MouseEvent; diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index a9b6920ae369e..11ef2c925f6c8 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["uiActions", "embeddable", "drilldowns"] + "requiredPlugins": ["uiActions", "embeddable", "drilldowns", "share"] } diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts index 447ceff4b8cb2..5f28ab109bfa0 100644 --- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -6,6 +6,7 @@ import { CoreStart, CoreSetup, Plugin } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { SharePluginStart } from '../../../../src/plugins/share/public'; import { DashboardDrilldownsService } from './services'; import { DrilldownsSetupContract, DrilldownsStartContract } from '../../drilldowns/public'; @@ -17,6 +18,7 @@ export interface SetupDependencies { export interface StartDependencies { uiActions: UiActionsStart; drilldowns: DrilldownsStartContract; + share: SharePluginStart; } // eslint-disable-next-line diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index 34d1ccafc8861..69c48a6f44fdb 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -35,6 +35,12 @@ export class DashboardDrilldownsService { const drilldowns = async () => (await core.getStartServices())[1].drilldowns; const getSavedObjectsClient = async () => (await core.getStartServices())[0].savedObjects.client; + const getNavigateToApp = async () => + (await core.getStartServices())[0].application.navigateToApp; + + const getGetUrlGenerator = async () => + // @ts-ignore + (await core.getStartServices())[1].share.urlGenerators.getUrlGenerator; const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays, drilldowns }); plugins.uiActions.registerAction(actionFlyoutCreateDrilldown); @@ -46,6 +52,8 @@ export class DashboardDrilldownsService { const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({ getSavedObjectsClient, + getGetUrlGenerator, + getNavigateToApp, }); plugins.drilldowns.registerDrilldown(dashboardToDashboardDrilldown); } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx index 2880f8dc9f902..b95d855ed52b3 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx @@ -82,8 +82,8 @@ export class CollectConfigContainer extends React.Component< } loadDashboards(searchString?: string) { - const currentDashboard = this.props.context.placeContext.embeddable.parent; - const currentDashboardId = currentDashboard && currentDashboard.id; + // const currentDashboard = this.props.context.placeContext.embeddable.parent; + // const currentDashboardId = currentDashboard && currentDashboard.id; this.setState({ searchString, isLoading: true }); this.props.params.getSavedObjectsClient().then(savedObjectsClient => { savedObjectsClient @@ -96,9 +96,9 @@ export class CollectConfigContainer extends React.Component< }) .then(({ savedObjects }) => { if (searchString === this.state.searchString) { - const dashboardList = savedObjects - .map(dashboardSavedObjectToMenuItem) - .filter(({ value }) => value !== currentDashboardId); + const dashboardList = savedObjects.map(dashboardSavedObjectToMenuItem); + // temporarily disable for dev purposes + // .filter(({ value }) => value !== currentDashboardId); this.setState({ dashboards: dashboardList, isLoading: false }); } }); @@ -114,7 +114,7 @@ export class CollectConfigContainer extends React.Component< activeDashboardId={config.dashboardId} dashboards={mergeDashboards(dashboards, selectedDashboard)} currentFilters={config.useCurrentDashboardFilters} - keepRange={config.useCurrentDashboardDataRange} + keepRange={config.useCurrentDashboardDateRange} isLoading={isLoading} onDashboardSelect={dashboardId => { onConfig({ ...config, dashboardId }); @@ -129,7 +129,7 @@ export class CollectConfigContainer extends React.Component< onKeepRangeToggle={() => onConfig({ ...config, - useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange, + useCurrentDashboardDateRange: !config.useCurrentDashboardDateRange, }) } /> diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 422d2d74ea99b..28c2a8ef726b4 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -7,6 +7,9 @@ import React from 'react'; import { CoreStart } from 'src/core/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; +import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; +import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; +import { VisualizeEmbeddable } from '../../../../../../../src/legacy/core_plugins/visualizations/public'; import { FactoryContext, ActionContext, Config, CollectConfigProps } from './types'; import { CollectConfigContainer } from './collect_config'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; @@ -16,10 +19,11 @@ import { txtGoToDashboard } from './i18n'; export interface Params { getSavedObjectsClient: () => Promise; getNavigateToApp: () => Promise; + getGetUrlGenerator: () => Promise; } export class DashboardToDashboardDrilldown - implements Drilldown { + implements Drilldown> { constructor(protected readonly params: Params) {} // TODO: public readonly places = ['dashboard']; @@ -40,7 +44,7 @@ export class DashboardToDashboardDrilldown public readonly createConfig = () => ({ dashboardId: '', - useCurrentDashboardDataRange: true, + useCurrentDashboardDateRange: true, useCurrentDashboardFilters: true, }); @@ -49,15 +53,21 @@ export class DashboardToDashboardDrilldown return true; }; - // it seems like this fn is being execute with the wrong arguments - // first param should be Config but its { config: Config; name: string; actionFactory: string; } ( I thtink ) - - // @ts-ignore - public readonly execute = async ({ config }: Config, context: ActionContext) => { + public readonly execute = async (config: Config, context: ActionContext) => { // todo - need to complete this - await this.params.getNavigateToApp().then(navigateToApp => { - navigateToApp('kibana', { path: `#/dashboard/${config.dashboardId}` }); + // console.log('DEBUG', config, context); + // need to change date range and filter based on config + const getUrlGenerator = await this.params.getGetUrlGenerator(); + const navigateToApp = await this.params.getNavigateToApp(); + const { timeRange, query, filters } = context.embeddable.getInput(); + + const dashboardPath = await getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({ + dashboardId: config.dashboardId, + timeRange, + query, + filters, }); - // window.location.hash = `#/dashboard/${config.dashboardId}`; + + navigateToApp(dashboardPath); }; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 74be9c328f7f2..800896dffa9e3 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -7,16 +7,17 @@ import { EmbeddableVisTriggerContext, EmbeddableContext, + IEmbeddable, } from '../../../../../../../src/plugins/embeddable/public'; import { UiActionsCollectConfigProps } from '../../../../../../../src/plugins/ui_actions/public'; export type FactoryContext = EmbeddableContext; -export type ActionContext = EmbeddableVisTriggerContext; +export type ActionContext = EmbeddableVisTriggerContext; export interface Config { dashboardId?: string; useCurrentDashboardFilters: boolean; - useCurrentDashboardDataRange: boolean; + useCurrentDashboardDateRange: boolean; } export type CollectConfigProps = UiActionsCollectConfigProps; From 0bbf360b1779feb959838df983c799e97f41334e Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Wed, 18 Mar 2020 01:27:22 -0500 Subject: [PATCH 09/32] remove note --- src/plugins/embeddable/public/lib/triggers/triggers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 651b64eddd58b..4b1d3575f2950 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -24,8 +24,6 @@ export interface EmbeddableContext { embeddable: IEmbeddable; } -// I think this could benefit using a type variable for IEmbeddable -// but I'm concerned that the change will touch many places export interface EmbeddableVisTriggerContext { embeddable: T; timeFieldName: string; From 636d9abbad2d394efec0731df7f4ee1054e1eb45 Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Wed, 18 Mar 2020 01:31:54 -0500 Subject: [PATCH 10/32] typefix --- x-pack/plugins/dashboard_enhanced/public/plugin.ts | 3 ++- .../services/drilldowns/dashboard_drilldowns_services.ts | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts index 5f28ab109bfa0..922e149ee86ea 100644 --- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -6,13 +6,14 @@ import { CoreStart, CoreSetup, Plugin } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { SharePluginStart } from '../../../../src/plugins/share/public'; +import { SharePluginStart, SharePluginSetup } from '../../../../src/plugins/share/public'; import { DashboardDrilldownsService } from './services'; import { DrilldownsSetupContract, DrilldownsStartContract } from '../../drilldowns/public'; export interface SetupDependencies { uiActions: UiActionsSetup; drilldowns: DrilldownsSetupContract; + share: SharePluginSetup; } export interface StartDependencies { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index 69c48a6f44fdb..8920bc6decda3 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -5,7 +5,7 @@ */ import { CoreSetup } from 'src/core/public'; -import { SetupDependencies } from '../../plugin'; +import { SetupDependencies, StartDependencies } from '../../plugin'; import { CONTEXT_MENU_TRIGGER, EmbeddableContext, @@ -16,7 +16,6 @@ import { OPEN_FLYOUT_ADD_DRILLDOWN, OPEN_FLYOUT_EDIT_DRILLDOWN, } from './actions'; -import { DrilldownsStartContract } from '../../../../drilldowns/public'; import { DashboardToDashboardDrilldown } from './dashboard_to_dashboard_drilldown'; declare module '../../../../../../src/plugins/ui_actions/public' { @@ -27,10 +26,7 @@ declare module '../../../../../../src/plugins/ui_actions/public' { } export class DashboardDrilldownsService { - async bootstrap( - core: CoreSetup<{ drilldowns: DrilldownsStartContract }>, - plugins: SetupDependencies - ) { + async bootstrap(core: CoreSetup, plugins: SetupDependencies) { const overlays = async () => (await core.getStartServices())[0].overlays; const drilldowns = async () => (await core.getStartServices())[1].drilldowns; const getSavedObjectsClient = async () => @@ -39,7 +35,6 @@ export class DashboardDrilldownsService { (await core.getStartServices())[0].application.navigateToApp; const getGetUrlGenerator = async () => - // @ts-ignore (await core.getStartServices())[1].share.urlGenerators.getUrlGenerator; const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays, drilldowns }); From 6ea19aa12776d5551afe35f57625b1fb1ce9b344 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 18 Mar 2020 18:41:03 +0100 Subject: [PATCH 11/32] fix navigation from dashboard to dashboard except for back button - that would be addressed separatly --- .../np_ready/dashboard_state_manager.ts | 7 +++++ .../dashboard/np_ready/url_helper.test.ts | 27 +++++++++++++++++ .../public/dashboard/np_ready/url_helper.ts | 21 ++++++++++++++ .../public/np_ready/public/index.ts | 1 - .../dashboard/public/url_generator.test.ts | 2 +- src/plugins/dashboard/public/url_generator.ts | 29 ++++++++++++------- .../drilldown.tsx | 25 +++++++++------- 7 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index f29721e3c3d5c..6b426d8d5344e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -50,6 +50,7 @@ import { ReduxLikeStateContainer, syncState, } from '../../../../../../plugins/kibana_utils/public'; +import { getDashboardIdFromUrl } from './url_helper'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the @@ -174,6 +175,12 @@ export class DashboardStateManager { // sync state required state container to be able to handle null // overriding set() so it could handle null coming from url if (state) { + // Skip this update if current dashboardId in the url is different from what we have in current instance of state manager + // Because dashboard is driven by angular, the destroy cycle happens async, + // And we should not to interfere into state change longer as this instance will be destroyed soon + const currentDashboardIdInUrl = getDashboardIdFromUrl(history.location.pathname); + if (currentDashboardIdInUrl !== this.savedDashboard.id) return; + this.stateContainer.set({ ...this.stateDefaults, ...state, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts index 60ca1b39d29d6..15079d86f2679 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts @@ -45,6 +45,7 @@ jest.mock('../legacy_imports', () => { import { addEmbeddableToDashboardUrl, + getDashboardIdFromUrl, getLensUrlFromDashboardAbsoluteUrl, getUrlVars, } from './url_helper'; @@ -115,4 +116,30 @@ describe('Dashboard URL Helper', () => { 'http://localhost:5601/app/kibana#/lens/edit/1244' ); }); + + it('getDashboardIdFromDashboardUrl', () => { + 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(getDashboardIdFromUrl(url)).toEqual(undefined); + + 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(getDashboardIdFromUrl(url)).toEqual('625357282'); + + url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182'; + expect(getDashboardIdFromUrl(url)).toEqual('777182'); + + url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); + + url = '/dashboard/test/?_g=(refreshInterval:'; + expect(getDashboardIdFromUrl(url)).toEqual('test'); + + url = 'dashboard/test/?_g=(refreshInterval:'; + expect(getDashboardIdFromUrl(url)).toEqual('test'); + + url = '/other-app/test/'; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); + }); }); 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 index 73383f2ff3f68..dfa1e77fa54cd 100644 --- 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 @@ -98,3 +98,24 @@ export function getLensUrlFromDashboardAbsoluteUrl( function getUrlWithoutQueryParams(url: string): string { return url.split('?')[0]; } + +/** + * Returns dashboard id from URL + * literally looks from id after `dashboard/` string and before `/`, `?` and end of string + * @param url to extract dashboardId from + * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z + * output: undefined + * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z + * output: 39292992 + */ +export function getDashboardIdFromUrl(url: string): string | undefined { + const [, match1, match2, match3] = url.match( + /dashboard\/(.*)\/|dashboard\/(.*)\?|dashboard\/(.*)$/ + ) ?? [ + undefined, // full match + undefined, // group1 - dashboardId is before `/` + undefined, // group2 - dashboardId is before `?` + undefined, // group3 - dashboardID is in the end + ]; + return match1 ?? match2 ?? match3 ?? undefined; +} diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 92c5af1413545..b59eb2277411c 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -41,7 +41,6 @@ export { VisTypeAlias, VisType } from './vis_types'; export { VisSavedObject } from './types'; export { Vis, VisParams, VisState } from './vis'; import { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable'; -export { VisualizeEmbeddable }; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; export { TypesService } from './vis_types/types_service'; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 5dfc47b694f60..8005619aad9d2 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -70,7 +70,7 @@ describe('dashboard url generator', () => { query: { query: 'bye', language: 'kuery' }, }); expect(url).toMatchInlineSnapshot( - `"xyz/app/kibana#/dashboard/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(time:(from:now-15m,mode:relative,to:now))"` + `"xyz/app/kibana#/dashboard/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(),time:(from:now-15m,mode:relative,to:now))"` ); }); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 174493eaad50e..f7af0f13bf1f2 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -17,7 +17,7 @@ * under the License. */ -import { TimeRange, Filter, Query } from '../../data/public'; +import { TimeRange, Filter, Query, esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public'; @@ -38,8 +38,7 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ timeRange?: TimeRange; /** * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has filters saved with it, this will _replace_ those filters. This will set - * app filters, not global filters. + * saved dashboard has filters saved with it, this will _replace_ those filters. */ filters?: Filter[]; /** @@ -61,24 +60,34 @@ export const createDirectAccessDashboardLinkGenerator = ( createUrl: async state => { const startServices = await getStartServices(); const useHash = state.useHash ?? startServices.useHashedUrl; - // const appBasePath = startServices.appBasePath; + const appBasePath = startServices.appBasePath; const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`; + const cleanEmptyStateKeys = (stateObj: Record) => { + Object.keys(stateObj).forEach(key => { + if (stateObj[key] === undefined) { + delete stateObj[key]; + } + }); + return stateObj; + }; + const appStateUrl = setStateToKbnUrl( STATE_STORAGE_KEY, - { + cleanEmptyStateKeys({ query: state.query, - filters: state.filters, - }, + filters: state.filters?.filter(f => !esFilters.isFilterPinned(f)), + }), { useHash }, - `kibana#/${hash}` // use appBasePath once dashboards is migrated, using 'kibana' for now + `${appBasePath}#/${hash}` ); return setStateToKbnUrl( GLOBAL_STATE_STORAGE_KEY, - { + cleanEmptyStateKeys({ time: state.timeRange, - }, + filters: state.filters?.filter(f => esFilters.isFilterPinned(f)), + }), { useHash }, appStateUrl ); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 28c2a8ef726b4..778953fe1e8f1 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -9,12 +9,13 @@ import { CoreStart } from 'src/core/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; -import { VisualizeEmbeddable } from '../../../../../../../src/legacy/core_plugins/visualizations/public'; +import { VisualizeEmbeddableContract } from '../../../../../../../src/legacy/core_plugins/visualizations/public'; import { FactoryContext, ActionContext, Config, CollectConfigProps } from './types'; import { CollectConfigContainer } from './collect_config'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; import { DrilldownsDrilldown as Drilldown } from '../../../../../drilldowns/public'; import { txtGoToDashboard } from './i18n'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; export interface Params { getSavedObjectsClient: () => Promise; @@ -23,7 +24,7 @@ export interface Params { } export class DashboardToDashboardDrilldown - implements Drilldown> { + implements Drilldown> { constructor(protected readonly params: Params) {} // TODO: public readonly places = ['dashboard']; @@ -53,21 +54,25 @@ export class DashboardToDashboardDrilldown return true; }; - public readonly execute = async (config: Config, context: ActionContext) => { - // todo - need to complete this - // console.log('DEBUG', config, context); - // need to change date range and filter based on config + public readonly execute = async ( + config: Config, + context: ActionContext + ) => { const getUrlGenerator = await this.params.getGetUrlGenerator(); const navigateToApp = await this.params.getNavigateToApp(); const { timeRange, query, filters } = context.embeddable.getInput(); const dashboardPath = await getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({ dashboardId: config.dashboardId, - timeRange, query, - filters, + timeRange: config.useCurrentDashboardDateRange ? timeRange : undefined, + filters: config.useCurrentDashboardFilters + ? filters + : filters?.filter(f => esFilters.isFilterPinned(f)), + }); + const dashboardHash = dashboardPath.split('#')[1]; + await navigateToApp('kibana', { + path: `#${dashboardHash}`, }); - - navigateToApp(dashboardPath); }; } From a431e86daf420d6ac8985073e0eeb2e1431d9dfb Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Fri, 20 Mar 2020 01:19:32 -0500 Subject: [PATCH 12/32] partial progress getting filters from action data --- .../filters/create_filters_from_event.ts | 5 + src/plugins/data/public/actions/index.ts | 4 +- .../public/actions/select_range_action.ts | 69 ++++---- .../data/public/actions/value_click_action.ts | 147 ++++++++++-------- src/plugins/data/public/index.ts | 2 + .../collect_config.tsx | 22 +-- .../drilldown.tsx | 35 ++++- .../dashboard_to_dashboard_drilldown/types.ts | 18 ++- 8 files changed, 183 insertions(+), 119 deletions(-) diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.ts b/src/plugins/data/public/actions/filters/create_filters_from_event.ts index e62945a592072..8b6135a246ce1 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_event.ts @@ -84,6 +84,11 @@ const createFilter = async (table: EventData['table'], columnIndex: number, rowI if (!column.meta || !column.meta.indexPatternId) { return; } + console.log('aggConfig', column.meta.indexPatternId); // eslint-disable-line + console.log( // eslint-disable-line + 'This fails, but not always', + await getIndexPatterns().get(column.meta.indexPatternId) + ); const aggConfig = deserializeAggConfig({ type: column.meta.type, aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, diff --git a/src/plugins/data/public/actions/index.ts b/src/plugins/data/public/actions/index.ts index cdb84ff13f25e..105229e2bd81a 100644 --- a/src/plugins/data/public/actions/index.ts +++ b/src/plugins/data/public/actions/index.ts @@ -19,5 +19,5 @@ export { ACTION_GLOBAL_APPLY_FILTER, createFilterAction } from './apply_filter_action'; export { createFiltersFromEvent } from './filters/create_filters_from_event'; -export { selectRangeAction } from './select_range_action'; -export { valueClickAction } from './value_click_action'; +export { selectRangeAction, selectRangeActionGetFilters } from './select_range_action'; +export { valueClickAction, valueClickActionGetFilters } from './value_click_action'; diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 6e1f16a09e803..c5358285acf1b 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -34,13 +34,45 @@ export interface SelectRangeActionContext { } async function isCompatible(context: SelectRangeActionContext) { - try { - return Boolean(await onBrushEvent(context.data)); - } catch { - return false; - } + // try { + return Boolean(await onBrushEvent(context.data)); + // } catch { + // return false; + // } } +export const selectRangeActionGetFilters = async ({ + timeFieldName, + data, +}: SelectRangeActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filter = await onBrushEvent(data); + + if (!filter) { + return; + } + + const selectedFilters = esFilters.mapAndFlattenFilters([filter]); + + return esFilters.extractTimeFilter(timeFieldName || '', selectedFilters); +}; + +const selectRangeActionExecute = ( + filterManager: FilterManager, + timeFilter: TimefilterContract +) => async ({ timeFieldName, data }: SelectRangeActionContext) => { + const { timeRangeFilter, restOfFilters } = + (await selectRangeActionGetFilters({ timeFieldName, data })) || {}; + + filterManager.addFilters(restOfFilters || []); + if (timeRangeFilter) { + esFilters.changeTimeFilter(timeFilter, timeRangeFilter); + } +}; + export function selectRangeAction( filterManager: FilterManager, timeFilter: TimefilterContract @@ -54,31 +86,6 @@ export function selectRangeAction( }); }, isCompatible, - execute: async ({ timeFieldName, data }: SelectRangeActionContext) => { - if (!(await isCompatible({ timeFieldName, data }))) { - throw new IncompatibleActionError(); - } - - const filter = await onBrushEvent(data); - - if (!filter) { - return; - } - - const selectedFilters = esFilters.mapAndFlattenFilters([filter]); - - if (timeFieldName) { - const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - timeFieldName, - selectedFilters - ); - filterManager.addFilters(restOfFilters); - if (timeRangeFilter) { - esFilters.changeTimeFilter(timeFilter, timeRangeFilter); - } - } else { - filterManager.addFilters(selectedFilters); - } - }, + execute: selectRangeActionExecute(filterManager, timeFilter), }); } diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 01c32e27da07d..d2f7d7cc0272f 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { RangeFilter } from 'src/plugins/data/public'; import { toMountPoint } from '../../../../plugins/kibana_react/public'; import { ActionByType, @@ -37,16 +38,90 @@ export interface ValueClickActionContext { } async function isCompatible(context: ValueClickActionContext) { - try { - const filters: Filter[] = - (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || - []; - return filters.length > 0; - } catch { - return false; - } + // try { + const filters: Filter[] = + (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || []; + return filters.length > 0; + // } catch { + // return false; + // } } +// this allows the user to select which filter to use +const filterSelectionFn = (filters: Filter[]): Promise => + new Promise(async resolve => { + const indexPatterns = await Promise.all( + filters.map(filter => { + return getIndexPatterns().get(filter.meta.index!); + }) + ); + + const overlay = getOverlays().openModal( + toMountPoint( + applyFiltersPopover( + filters, + indexPatterns, + () => { + overlay.close(); + resolve([]); + }, + (filterSelection: Filter[]) => { + overlay.close(); + resolve(filterSelection); + } + ) + ), + { + 'data-test-subj': 'selectFilterOverlay', + } + ); + }); + +// given a ValueClickActionContext, returns timeRangeFilter and Filters +export const valueClickActionGetFilters = async ( + { timeFieldName, data }: ValueClickActionContext, + filterSelection: (filters: Filter[]) => Promise = async (filters: Filter[]) => filters +): Promise<{ + timeRangeFilter?: RangeFilter; + restOfFilters: Filter[]; +}> => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + // + const filters: Filter[] = (await createFiltersFromEvent(data.data || [data], data.negate)) || []; + + let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); + // filters + + if (selectedFilters.length > 1) { + selectedFilters = await filterSelection(filters); + } + + return esFilters.extractTimeFilter(timeFieldName || '', selectedFilters); +}; + +// gets and applies the filters +export const valueClickActionExecute = ( + filterManager: FilterManager, + timeFilter: TimefilterContract, + filterSelection: (filters: Filter[]) => Promise = async (filters: Filter[]) => filters +) => async ({ timeFieldName, data }: ValueClickActionContext) => { + const { timeRangeFilter, restOfFilters } = await valueClickActionGetFilters( + { + timeFieldName, + data, + }, + filterSelection + ); + + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + esFilters.changeTimeFilter(timeFilter, timeRangeFilter); + } +}; + export function valueClickAction( filterManager: FilterManager, timeFilter: TimefilterContract @@ -60,60 +135,6 @@ export function valueClickAction( }); }, isCompatible, - execute: async ({ timeFieldName, data }: ValueClickActionContext) => { - if (!(await isCompatible({ timeFieldName, data }))) { - throw new IncompatibleActionError(); - } - - const filters: Filter[] = - (await createFiltersFromEvent(data.data || [data], data.negate)) || []; - - let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); - - if (selectedFilters.length > 1) { - const indexPatterns = await Promise.all( - filters.map(filter => { - return getIndexPatterns().get(filter.meta.index!); - }) - ); - - const filterSelectionPromise: Promise = new Promise(resolve => { - const overlay = getOverlays().openModal( - toMountPoint( - applyFiltersPopover( - filters, - indexPatterns, - () => { - overlay.close(); - resolve([]); - }, - (filterSelection: Filter[]) => { - overlay.close(); - resolve(filterSelection); - } - ) - ), - { - 'data-test-subj': 'selectFilterOverlay', - } - ); - }); - - selectedFilters = await filterSelectionPromise; - } - - if (timeFieldName) { - const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - timeFieldName, - selectedFilters - ); - filterManager.addFilters(restOfFilters); - if (timeRangeFilter) { - esFilters.changeTimeFilter(timeFilter, timeRangeFilter); - } - } else { - filterManager.addFilters(selectedFilters); - } - }, + execute: valueClickActionExecute(filterManager, timeFilter, filterSelectionFn), }); } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 58bd9a5ab05d7..e4d708c3f28a8 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -450,6 +450,8 @@ export { getKbnTypeNames, } from '../common'; +export { valueClickActionGetFilters, selectRangeActionGetFilters } from './actions'; + /* * Plugin setup */ diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx index b95d855ed52b3..3866fe95f5011 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx @@ -9,9 +9,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { debounce, findIndex } from 'lodash'; import { CollectConfigProps } from './types'; import { DashboardDrilldownConfig } from '../../../components/dashboard_drilldown_config'; -import { Params } from './drilldown'; import { SimpleSavedObject } from '../../../../../../../src/core/public'; -import { IEmbeddable } from '../../../../../../../src/plugins/embeddable/public'; const mergeDashboards = ( dashboards: Array>, @@ -33,16 +31,6 @@ const dashboardSavedObjectToMenuItem = ( label: savedObject.attributes.title, }); -export interface CollectConfigContainerProps extends CollectConfigProps { - params: Params; - context: { - place: string; - placeContext: { - embeddable: IEmbeddable; - }; - }; -} - interface CollectConfigContainerState { dashboards: Array>; searchString?: string; @@ -52,7 +40,7 @@ interface CollectConfigContainerState { } export class CollectConfigContainer extends React.Component< - CollectConfigContainerProps, + CollectConfigProps, CollectConfigContainerState > { state = { @@ -70,7 +58,7 @@ export class CollectConfigContainer extends React.Component< loadSelectedDashboard() { const { config } = this.props; - this.props.params.getSavedObjectsClient().then(savedObjectsClient => { + this.props.deps.getSavedObjectsClient().then(savedObjectsClient => { if (config.dashboardId) { savedObjectsClient .get<{ title: string }>('dashboard', config.dashboardId) @@ -85,7 +73,7 @@ export class CollectConfigContainer extends React.Component< // const currentDashboard = this.props.context.placeContext.embeddable.parent; // const currentDashboardId = currentDashboard && currentDashboard.id; this.setState({ searchString, isLoading: true }); - this.props.params.getSavedObjectsClient().then(savedObjectsClient => { + this.props.deps.getSavedObjectsClient().then(savedObjectsClient => { savedObjectsClient .find<{ title: string }>({ type: 'dashboard', @@ -114,7 +102,7 @@ export class CollectConfigContainer extends React.Component< activeDashboardId={config.dashboardId} dashboards={mergeDashboards(dashboards, selectedDashboard)} currentFilters={config.useCurrentDashboardFilters} - keepRange={config.useCurrentDashboardDateRange} + keepRange={config.useCurrentDashboardDataRange} isLoading={isLoading} onDashboardSelect={dashboardId => { onConfig({ ...config, dashboardId }); @@ -129,7 +117,7 @@ export class CollectConfigContainer extends React.Component< onKeepRangeToggle={() => onConfig({ ...config, - useCurrentDashboardDateRange: !config.useCurrentDashboardDateRange, + useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange, }) } /> diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 778953fe1e8f1..f1be2e7b72ca4 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -15,7 +15,11 @@ import { CollectConfigContainer } from './collect_config'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; import { DrilldownsDrilldown as Drilldown } from '../../../../../drilldowns/public'; import { txtGoToDashboard } from './i18n'; -import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { + esFilters, + valueClickActionGetFilters, + selectRangeActionGetFilters, +} from '../../../../../../../src/plugins/data/public'; export interface Params { getSavedObjectsClient: () => Promise; @@ -38,14 +42,14 @@ export class DashboardToDashboardDrilldown public readonly euiIcon = 'dashboardApp'; private readonly ReactCollectConfig: React.FC = props => ( - + ); public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); public readonly createConfig = () => ({ dashboardId: '', - useCurrentDashboardDateRange: true, + useCurrentDashboardDataRange: true, useCurrentDashboardFilters: true, }); @@ -58,19 +62,42 @@ export class DashboardToDashboardDrilldown config: Config, context: ActionContext ) => { + console.log('drilldown execute'); // eslint-disable-line const getUrlGenerator = await this.params.getGetUrlGenerator(); const navigateToApp = await this.params.getNavigateToApp(); const { timeRange, query, filters } = context.embeddable.getInput(); + // @ts-ignore + if (context.data.range) { + // look up by range + const { restOfFilters, timeRangeFilter } = + (await selectRangeActionGetFilters({ + timeFieldName: context.timeFieldName, + data: context.data, + })) || {}; + console.log('select range action filters', restOfFilters, timeRangeFilter); // eslint-disable-line + // selectRangeActionGetFilters + } else { + const { restOfFilters, timeRangeFilter } = await valueClickActionGetFilters({ + timeFieldName: context.timeFieldName, + data: context.data, + }); + console.log('value click action filters', restOfFilters, timeRangeFilter); // eslint-disable-line + } + const dashboardPath = await getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({ dashboardId: config.dashboardId, query, - timeRange: config.useCurrentDashboardDateRange ? timeRange : undefined, + // todo - how to get destination dashboard timerange? + timeRange: config.useCurrentDashboardDataRange ? timeRange : undefined, filters: config.useCurrentDashboardFilters ? filters : filters?.filter(f => esFilters.isFilterPinned(f)), }); + const dashboardHash = dashboardPath.split('#')[1]; + + console.log('dashboard hash', dashboardHash); // eslint-disable-line await navigateToApp('kibana', { path: `#${dashboardHash}`, }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 800896dffa9e3..bbc9a815dc79e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CoreStart } from 'src/core/public'; +import { SharePluginStart } from 'src/plugins/share/public'; import { EmbeddableVisTriggerContext, EmbeddableContext, @@ -17,7 +19,19 @@ export type ActionContext = EmbeddableVisTr export interface Config { dashboardId?: string; useCurrentDashboardFilters: boolean; - useCurrentDashboardDateRange: boolean; + useCurrentDashboardDataRange: boolean; } -export type CollectConfigProps = UiActionsCollectConfigProps; +export interface CollectConfigProps extends UiActionsCollectConfigProps { + deps: { + getSavedObjectsClient: () => Promise; + getNavigateToApp: () => Promise; + getGetUrlGenerator: () => Promise; + }; + context: { + place: string; + placeContext: { + embeddable: IEmbeddable; + }; + }; +} From 4217b18e304adcf8271b39b85e5347d712c40b4f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 20 Mar 2020 11:53:12 +0100 Subject: [PATCH 13/32] fix issue with getIndexPatterns undefined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit we can’t import those functions as static functions, instead we have to expose them on plugin contract because they are statefull --- src/plugins/data/public/index.ts | 2 -- src/plugins/data/public/mocks.ts | 2 ++ src/plugins/data/public/plugin.ts | 10 +++++++++- src/plugins/data/public/types.ts | 8 +++++++- x-pack/plugins/dashboard_enhanced/kibana.json | 2 +- x-pack/plugins/dashboard_enhanced/public/plugin.ts | 2 ++ .../drilldowns/dashboard_drilldowns_services.ts | 5 ++++- .../dashboard_to_dashboard_drilldown/drilldown.tsx | 13 +++++++------ 8 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e4d708c3f28a8..58bd9a5ab05d7 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -450,8 +450,6 @@ export { getKbnTypeNames, } from '../common'; -export { valueClickActionGetFilters, selectRangeActionGetFilters } from './actions'; - /* * Plugin setup */ diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index c5cff1c5c68d9..395ba81bd48b7 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -56,6 +56,8 @@ const createStartContract = (): Start => { const startContract = { actions: { createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), + selectRangeActionGetFilters: jest.fn(), + valueClickActionGetFilters: jest.fn(), }, autocomplete: autocompleteMock, getSuggestions: jest.fn(), diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 1dbaed6aac6bd..bfcb60ccd2a0d 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -54,7 +54,13 @@ import { VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, } from '../../ui_actions/public'; -import { ACTION_GLOBAL_APPLY_FILTER, createFilterAction, createFiltersFromEvent } from './actions'; +import { + ACTION_GLOBAL_APPLY_FILTER, + createFilterAction, + createFiltersFromEvent, + selectRangeActionGetFilters, + valueClickActionGetFilters, +} from './actions'; import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action'; import { selectRangeAction, @@ -152,6 +158,8 @@ export class DataPublicPlugin implements Plugin, plugins: SetupDependencies) { + bootstrap(core: CoreSetup, plugins: SetupDependencies) { const overlays = async () => (await core.getStartServices())[0].overlays; const drilldowns = async () => (await core.getStartServices())[1].drilldowns; const getSavedObjectsClient = async () => @@ -37,6 +37,8 @@ export class DashboardDrilldownsService { const getGetUrlGenerator = async () => (await core.getStartServices())[1].share.urlGenerators.getUrlGenerator; + const getDataPluginActions = async () => (await core.getStartServices())[1].data.actions; + const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays, drilldowns }); plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown); @@ -47,6 +49,7 @@ export class DashboardDrilldownsService { getSavedObjectsClient, getGetUrlGenerator, getNavigateToApp, + getDataPluginActions, }); plugins.drilldowns.registerDrilldown(dashboardToDashboardDrilldown); } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index f1be2e7b72ca4..ddbaba95a7956 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -10,21 +10,18 @@ import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_reac import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; import { VisualizeEmbeddableContract } from '../../../../../../../src/legacy/core_plugins/visualizations/public'; -import { FactoryContext, ActionContext, Config, CollectConfigProps } from './types'; +import { ActionContext, CollectConfigProps, Config, FactoryContext } from './types'; import { CollectConfigContainer } from './collect_config'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; import { DrilldownsDrilldown as Drilldown } from '../../../../../drilldowns/public'; import { txtGoToDashboard } from './i18n'; -import { - esFilters, - valueClickActionGetFilters, - selectRangeActionGetFilters, -} from '../../../../../../../src/plugins/data/public'; +import { DataPublicPluginStart, esFilters } from '../../../../../../../src/plugins/data/public'; export interface Params { getSavedObjectsClient: () => Promise; getNavigateToApp: () => Promise; getGetUrlGenerator: () => Promise; + getDataPluginActions: () => Promise; } export class DashboardToDashboardDrilldown @@ -65,6 +62,10 @@ export class DashboardToDashboardDrilldown console.log('drilldown execute'); // eslint-disable-line const getUrlGenerator = await this.params.getGetUrlGenerator(); const navigateToApp = await this.params.getNavigateToApp(); + const { + selectRangeActionGetFilters, + valueClickActionGetFilters, + } = await this.params.getDataPluginActions(); const { timeRange, query, filters } = context.embeddable.getInput(); // @ts-ignore From 177822fb8f15029a28c667ce5eb58007231bbd3c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 20 Mar 2020 12:39:22 +0100 Subject: [PATCH 14/32] fix filter / time passing into url --- src/plugins/data/public/index.ts | 2 + .../data/public/query/timefilter/index.ts | 2 +- .../timefilter/lib/change_time_filter.ts | 10 ++++- .../public/lib/triggers/triggers.ts | 1 + .../drilldown.tsx | 39 +++++++++++++------ 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 58bd9a5ab05d7..1e89b23fe510f 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -59,6 +59,7 @@ import { changeTimeFilter, mapAndFlattenFilters, extractTimeFilter, + convertRangeFilterToTimeRangeString, } from './query'; // Filter helpers namespace: @@ -96,6 +97,7 @@ export const esFilters = { onlyDisabledFiltersChanged, changeTimeFilter, + convertRangeFilterToTimeRangeString, mapAndFlattenFilters, extractTimeFilter, }; diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index a6260e782c12f..3b7de45799a00 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -23,5 +23,5 @@ export * from './types'; export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; export { getTime } from './get_time'; -export { changeTimeFilter } from './lib/change_time_filter'; +export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter'; export { extractTimeFilter } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts b/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts index 8da83580ef5d6..cbbf2f2754312 100644 --- a/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts +++ b/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts @@ -20,7 +20,7 @@ import moment from 'moment'; import { keys } from 'lodash'; import { TimefilterContract } from '../../timefilter'; -import { RangeFilter } from '../../../../common'; +import { RangeFilter, TimeRange } from '../../../../common'; export function convertRangeFilterToTimeRange(filter: RangeFilter) { const key = keys(filter.range)[0]; @@ -32,6 +32,14 @@ export function convertRangeFilterToTimeRange(filter: RangeFilter) { }; } +export function convertRangeFilterToTimeRangeString(filter: RangeFilter): TimeRange { + const { from, to } = convertRangeFilterToTimeRange(filter); + return { + from: from?.toISOString(), + to: to?.toISOString(), + }; +} + export function changeTimeFilter(timeFilter: TimefilterContract, filter: RangeFilter) { timeFilter.setTime(convertRangeFilterToTimeRange(filter)); } diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 4b1d3575f2950..d7da47a9317a0 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -30,6 +30,7 @@ export interface EmbeddableVisTriggerContext ) => { - console.log('drilldown execute'); // eslint-disable-line const getUrlGenerator = await this.params.getGetUrlGenerator(); const navigateToApp = await this.params.getNavigateToApp(); const { selectRangeActionGetFilters, valueClickActionGetFilters, } = await this.params.getDataPluginActions(); - const { timeRange, query, filters } = context.embeddable.getInput(); + const { + timeRange: currentTimeRange, + query, + filters: currentFilters, + } = context.embeddable.getInput(); + + // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) + // otherwise preserve only pinned + const filters = + (config.useCurrentDashboardFilters + ? currentFilters + : currentFilters?.filter(f => esFilters.isFilterPinned(f))) ?? []; + + // if useCurrentDashboardDataRange is enabled, then preserve current time range + // if undefined is passed, then destination dashboard will figure out time range itself + // for brush event this time range would be overwritten + let timeRange = config.useCurrentDashboardDataRange ? currentTimeRange : undefined; - // @ts-ignore if (context.data.range) { // look up by range const { restOfFilters, timeRangeFilter } = @@ -76,29 +90,30 @@ export class DashboardToDashboardDrilldown timeFieldName: context.timeFieldName, data: context.data, })) || {}; - console.log('select range action filters', restOfFilters, timeRangeFilter); // eslint-disable-line - // selectRangeActionGetFilters + filters.push(...(restOfFilters || [])); + if (timeRangeFilter) { + timeRange = esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter); + } } else { const { restOfFilters, timeRangeFilter } = await valueClickActionGetFilters({ timeFieldName: context.timeFieldName, data: context.data, }); - console.log('value click action filters', restOfFilters, timeRangeFilter); // eslint-disable-line + filters.push(...(restOfFilters || [])); + if (timeRangeFilter) { + timeRange = esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter); + } } const dashboardPath = await getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({ dashboardId: config.dashboardId, query, - // todo - how to get destination dashboard timerange? - timeRange: config.useCurrentDashboardDataRange ? timeRange : undefined, - filters: config.useCurrentDashboardFilters - ? filters - : filters?.filter(f => esFilters.isFilterPinned(f)), + timeRange, + filters, }); const dashboardHash = dashboardPath.split('#')[1]; - console.log('dashboard hash', dashboardHash); // eslint-disable-line await navigateToApp('kibana', { path: `#${dashboardHash}`, }); From f7b7add5118eb99fe23aff6b7d473989d7c10846 Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Sat, 21 Mar 2020 00:31:09 -0500 Subject: [PATCH 15/32] typefix --- .../dashboard_to_dashboard_drilldown/types.ts | 10 ++++------ x-pack/plugins/drilldowns/public/index.ts | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 22c4c52ffbd56..39d6507e277d8 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -6,6 +6,7 @@ import { CoreStart } from 'src/core/public'; import { SharePluginStart } from 'src/plugins/share/public'; +import { DrilldownFactoryContext } from '../../../../../drilldowns/public'; import { EmbeddableVisTriggerContext, EmbeddableContext, @@ -28,10 +29,7 @@ export interface CollectConfigProps extends UiActionsCollectConfigProps getNavigateToApp: () => Promise; getGetUrlGenerator: () => Promise; }; - context: { - place: string; - placeContext: { - embeddable: IEmbeddable; - }; - }; + context: DrilldownFactoryContext<{ + embeddable: IEmbeddable; + }>; } diff --git a/x-pack/plugins/drilldowns/public/index.ts b/x-pack/plugins/drilldowns/public/index.ts index 044e29c671de4..17ccd60e17ce4 100644 --- a/x-pack/plugins/drilldowns/public/index.ts +++ b/x-pack/plugins/drilldowns/public/index.ts @@ -17,4 +17,4 @@ export function plugin() { return new DrilldownsPlugin(); } -export { DrilldownDefinition } from './types'; +export { DrilldownDefinition, DrilldownFactoryContext } from './types'; From bd7a91c50232aee9b94120f7a3c8a00fda7f474d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Sat, 21 Mar 2020 12:55:04 +0100 Subject: [PATCH 16/32] dashboard to dashboard drilldown functional test and back button fix --- config/kibana.yml | 2 +- .../np_ready/dashboard_state_manager.ts | 4 +- .../ui/public/chrome/api/sub_url_hooks.js | 10 +- .../lib/panel/panel_header/panel_header.tsx | 5 +- .../public/context_menu/open_context_menu.tsx | 6 +- .../public/triggers/trigger_internal.ts | 4 +- .../functional/page_objects/dashboard_page.ts | 14 + .../action_wizard/action_wizard.test.tsx | 6 +- .../action_wizard/action_wizard.tsx | 8 +- .../dashboard_drilldown_config.tsx | 1 + .../flyout_create_drilldown.tsx | 1 + .../flyout_edit_drilldown.tsx | 1 + .../drilldown_hello_bar.tsx | 2 +- .../flyout_drilldown_wizard.tsx | 1 + .../form_drilldown_wizard.test.tsx | 8 +- .../form_drilldown_wizard.tsx | 2 +- .../list_manage_drilldowns.tsx | 10 +- .../drilldowns/dashboard_drilldowns.ts | 166 ++++++++++++ .../apps/dashboard/drilldowns/index.ts | 13 + .../test/functional/apps/dashboard/index.ts | 1 + .../dashboard/drilldowns/data.json.gz | Bin 0 -> 2640 bytes .../dashboard/drilldowns/mappings.json | 244 ++++++++++++++++++ .../services/dashboard/drilldowns_manage.ts | 97 +++++++ .../functional/services/dashboard/index.ts | 8 + .../dashboard/panel_drilldown_actions.ts | 68 +++++ x-pack/test/functional/services/index.ts | 6 + 26 files changed, 659 insertions(+), 29 deletions(-) create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/index.ts create mode 100644 x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz create mode 100644 x-pack/test/functional/es_archives/dashboard/drilldowns/mappings.json create mode 100644 x-pack/test/functional/services/dashboard/drilldowns_manage.ts create mode 100644 x-pack/test/functional/services/dashboard/index.ts create mode 100644 x-pack/test/functional/services/dashboard/panel_drilldown_actions.ts diff --git a/config/kibana.yml b/config/kibana.yml index 7b11a32bbdb42..13728a7bf212a 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -112,4 +112,4 @@ #i18n.locale: "en" # Enables "Drilldowns" functionality on dashboard. Set to false by default. -# xpack.dashboardEnhanced.drilldowns.enabled: false +xpack.dashboardEnhanced.drilldowns.enabled: true diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index 6b426d8d5344e..d26948e06c8cb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -236,8 +236,8 @@ export class DashboardStateManager { if ( !_.isEqual( - convertedPanelStateMap[panelState.explicitInput.id], - savedDashboardPanelMap[panelState.explicitInput.id] + _.omit(convertedPanelStateMap[panelState.explicitInput.id], 'version'), + _.omit(savedDashboardPanelMap[panelState.explicitInput.id], 'version') ) ) { // A panel was changed diff --git a/src/legacy/ui/public/chrome/api/sub_url_hooks.js b/src/legacy/ui/public/chrome/api/sub_url_hooks.js index 27d147b1ffc72..ca4a4e6d120fa 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -17,8 +17,6 @@ * under the License. */ -import url from 'url'; - import { unhashUrl } from '../../../../../plugins/kibana_utils/public'; import { toastNotifications } from '../../notify/toasts'; import { npSetup } from '../../new_platform'; @@ -47,14 +45,14 @@ export function registerSubUrlHooks(angularModule, internals) { } $rootScope.$on('$locationChangeStart', (e, newUrl) => { + const getHash = url => url.split('#')[1] || ''; + // This handler fixes issue #31238 where browser back navigation // fails due to angular 1.6 parsing url encoded params wrong. - const parsedAbsUrl = url.parse($location.absUrl()); - const absUrlHash = parsedAbsUrl.hash ? parsedAbsUrl.hash.slice(1) : ''; + const absUrlHash = getHash($location.absUrl()); const decodedAbsUrlHash = decodeURIComponent(absUrlHash); - const parsedNewUrl = url.parse(newUrl); - const newHash = parsedNewUrl.hash ? parsedNewUrl.hash.slice(1) : ''; + const newHash = getHash(newUrl); const decodedHash = decodeURIComponent(newHash); if (absUrlHash !== newHash && decodedHash === decodedAbsUrlHash) { diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index ed1ae739502a7..416f78824252d 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -151,7 +151,10 @@ export function PanelHeader({ {renderBadges(badges, embeddable)} {!isViewMode && !!drilldownCount && ( - + {drilldownCount} )} diff --git a/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx b/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx index 4d794618e85ab..c723388c021e9 100644 --- a/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx +++ b/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx @@ -149,7 +149,11 @@ export function openContextMenu( anchorPosition="downRight" withTitle > - + , container ); diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index 9885ed3abe93b..c65e9ad29a5a2 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -75,6 +75,8 @@ export class TriggerInternal { title: this.trigger.title, closeMenu: () => session.close(), }); - const session = openContextMenu([panel]); + const session = openContextMenu([panel], { + 'data-test-subj': 'multipleActionsContextMenu', + }); } } diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 0f01097cf50dc..4bdd4cbcd1433 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -510,6 +510,20 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide return checkList.filter(viz => viz.isPresent === false).map(viz => viz.name); } + + public async getPanelDrilldownCount(panelIndex = 0): Promise { + log.debug('getPanelDrilldownCount'); + const panel = (await this.getDashboardPanels())[panelIndex]; + try { + const count = await panel.findByCssSelector( + '[data-test-subj="embeddablePanelDrilldownCount"]' + ); + return Number.parseInt(await count.getVisibleText(), 10); + } catch (e) { + // if not found then this is 0 (we don't show badge with 0) + return 0; + } + } } return new DashboardPage(); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx index cc56714fcb2f8..f43d832b1edae 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx @@ -18,7 +18,7 @@ test('Pick and configure action', () => { const screen = render(); // check that all factories are displayed to pick - expect(screen.getAllByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).toHaveLength(2); + expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2); // select URL one fireEvent.click(screen.getByText(/Go to URL/i)); @@ -43,8 +43,8 @@ test('If only one actions factory is available then actionFactory selection is e const screen = render(); // check that no factories are displayed to pick from - expect(screen.queryByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).not.toBeInTheDocument(); - expect(screen.queryByTestId(TEST_SUBJ_SELECTED_ACTION_FACTORY)).toBeInTheDocument(); + expect(screen.queryByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).not.toBeInTheDocument(); + expect(screen.queryByTestId(new RegExp(TEST_SUBJ_SELECTED_ACTION_FACTORY))).toBeInTheDocument(); // Input url const URL = 'https://elastic.co'; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx index 846f6d41eb30d..b4aa3c1ba4608 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -105,7 +105,7 @@ interface SelectedActionFactoryProps { onDeselect: () => void; } -export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selected-action-factory'; +export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selectedActionFactory'; const SelectedActionFactory: React.FC = ({ actionFactory, @@ -118,7 +118,7 @@ const SelectedActionFactory: React.FC = ({ return (
@@ -159,7 +159,7 @@ interface ActionFactorySelectorProps { onActionFactorySelected: (actionFactory: ActionFactory) => void; } -export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'action-factory-item'; +export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'actionFactoryItem'; const ActionFactorySelector: React.FC = ({ actionFactories, @@ -181,7 +181,7 @@ const ActionFactorySelector: React.FC = ({ onActionFactorySelected(actionFactory)} > {actionFactory.getIconType(context) && ( diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx index da5a3cfa34697..386664da4f625 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx @@ -51,6 +51,7 @@ export const DashboardDrilldownConfig: React.FC = isLoading={isLoading} singleSelection={{ asPlainText: true }} fullWidth + data-test-subj={'dashboardDrilldownSelectDashboard'} /> {!!onCurrentFiltersToggle && ( diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index 792238adff313..f0926af2a348d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -68,6 +68,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType void; } -export const WELCOME_MESSAGE_TEST_SUBJ = 'drilldowns-welcome-message-test-subj'; +export const WELCOME_MESSAGE_TEST_SUBJ = 'drilldownsWelcomeMessage'; export const DrilldownHelloBar: React.FC = ({ docsLink, diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index faa965a98a4bb..8541aae06ff0c 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -79,6 +79,7 @@ export function FlyoutDrilldownWizard {mode === 'edit' ? txtEditDrilldownButtonLabel : txtCreateDrilldownButtonLabel} diff --git a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx index 4560773cc8a6d..d9c53ae6f737a 100644 --- a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx @@ -24,9 +24,7 @@ describe('', () => { render(, div); - const input = div.querySelector( - '[data-test-subj="dynamicActionNameInput"]' - ) as HTMLInputElement; + const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement; expect(input?.value).toBe(''); }); @@ -36,9 +34,7 @@ describe('', () => { render(, div); - const input = div.querySelector( - '[data-test-subj="dynamicActionNameInput"]' - ) as HTMLInputElement; + const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement; expect(input?.value).toBe('foo'); diff --git a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx index 4c9a7a2e514c6..9778e1dc6e95a 100644 --- a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -47,7 +47,7 @@ export const FormDrilldownWizard: React.FC = ({ value={name} disabled={onNameChange === noopFn} onChange={event => onNameChange(event.target.value)} - data-test-subj="dynamicActionNameInput" + data-test-subj="drilldownNameInput" /> ); diff --git a/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx index 5a15781a1faf2..ab51c0a829ed3 100644 --- a/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx +++ b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx @@ -40,7 +40,7 @@ export interface ListManageDrilldownsProps { const noop = () => {}; -export const TEST_SUBJ_DRILLDOWN_ITEM = 'list-manage-drilldowns-item'; +export const TEST_SUBJ_DRILLDOWN_ITEM = 'listManageDrilldownsItem'; export function ListManageDrilldowns({ drilldowns, @@ -56,6 +56,7 @@ export function ListManageDrilldowns({ name: 'Name', truncateText: true, width: '50%', + 'data-test-subj': 'drilldownListItemName', }, { name: 'Action', @@ -107,7 +108,12 @@ export function ListManageDrilldowns({ {txtCreateDrilldown} ) : ( - onDelete(selectedDrilldowns)}> + onDelete(selectedDrilldowns)} + data-test-subj={'listManageDeleteDrilldowns'} + > {txtDeleteDrilldowns(selectedDrilldowns.length)} )} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts new file mode 100644 index 0000000000000..1121921c7d315 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const DASHBOARD_WITH_PIE_CHART_NAME = 'Dashboard with Pie Chart'; +const DASHBOARD_WITH_AREA_CHART_NAME = 'Dashboard With Area Chart'; + +const DRILLDOWN_TO_PIE_CHART_NAME = 'Go to pie chart dashboard'; +const DRILLDOWN_TO_AREA_CHART_NAME = 'Go to area chart dashboard'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); + const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const pieChart = getService('pieChart'); + const log = getService('log'); + const browser = getService('browser'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + + describe('Dashboard Drilldowns', function() { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('dashboard/drilldowns'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + }); + + after(async () => { + await esArchiver.unload('dashboard/drilldowns'); + }); + + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode(DASHBOARD_WITH_PIE_CHART_NAME); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard(DASHBOARD_WITH_PIE_CHART_NAME, { + saveAsNew: false, + waitDialogIsClosed: true, + }); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.filterOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); + + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard(DASHBOARD_WITH_AREA_CHART_NAME); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + originalTimeRangeDurationHours + ); + }); + }); + + // utils which shouldn't be a part of test flow, but also too specific to be moved to pageobject or service + async function brushAreaChart() { + const areaChart = await testSubjects.find('visualizationLoader'); + expect(await areaChart.getAttribute('data-title')).to.be('Visualization漢字 AreaChart'); + await browser.dragAndDrop( + { + location: areaChart, + offset: { + x: 150, + y: 100, + }, + }, + { + location: areaChart, + offset: { + x: 200, + y: 100, + }, + } + ); + } + + async function navigateWithinDashboard(navigationTrigger: () => Promise) { + // before executing action which would trigger navigation: remember current dashboard id in url + const oldDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + // execute navigation action + await navigationTrigger(); + // wait until dashboard navigates to a new dashboard with area chart + await retry.waitFor('navigate to different dashboard', async () => { + const newDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + return typeof newDashboardId === 'string' && oldDashboardId !== newDashboardId; + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + } +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts new file mode 100644 index 0000000000000..ab273018dc3f7 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/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. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('drilldowns', function() { + this.tags(['skipFirefox']); + loadTestFile(require.resolve('./dashboard_drilldowns')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 5f4d3fcbd8c04..b734dea1a80a5 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -10,5 +10,6 @@ export default function({ loadTestFile }: FtrProviderContext) { this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./drilldowns')); }); } diff --git a/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz b/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..febf312799eee0de78a6de2226c17d4fe6183c68 GIT binary patch literal 2640 zcmV-W3a|AaiwFoL|8-sf17u-zVJ>QOZ*BnXT-|cpxD~$7Q?R=1L^3JKlBKISwl{U_ z*d9rn%*OI?AQBYOkOY?iEju1PM|<7-zCfo>)0y@;dH{kHDanqr>jM^`AYmPU%k42y0|xlr6?d@kYI24p?vkPzy9Mt|NPs(|NfWeG2&=~W&!?o zedJtJ#wi3{BR{6LbspqB+2~_*gWZeSpRvQ=V~#4_2&OPf9DgiX`{VfHT=aH7&19H1 zhSNqs=ZqufH-A5Eyq-3GPH}Pv4fpD&&3Hnhd5I#Q%5H|?gS#)-UXZ|bP~t)pPg1;G zLYqedjuW6*pd{Z1cfZqL5!12G#7VRCNwdhE-q2R2?|DZA@1=|z_4loix7t$ z^1~oF!#6l6+J%PDd@io9ggIO}qE%(Up=rb=i8($FktBu~4C1V@1sY%V(ysOb5y$Bq z?p*I_mO7s?-w@CvILJDh-EquhJBh%S5JEt1o2HN(-*OB-uc0M?@o)r| z$Y@3Iey1pHe!!r82L*3Ya^!<0G=}K|ti0@KH6D|Chv8cF^Rh=X;0phAcuSZPSt!GG zCzJ-9#1+~Zo`YMjNDSo4x(`SM#*ux8)XivM3J4)LERrtgXzIP9o6-6t{#6MVl>4kw?fSsbHnO zuPASWRH)a=ug_@`3ZbwlpH{;xashB8${M!>GHH^l*(ZaoR;OjkTLN^K z#qP9`5j?>@mjn_*owXMWuM?(8Pz2?EBC)4XCI3^Va)Cf!DEJC z{4T)RK-0!@mvB1A)Q7;b=LvDY9ZccrmP2Xc5-^9V#sr97CfFfLp-LHXiPZj1=qLfB z8H3(&5)D_N@$HaU>8Xl&QhFwO5w;O8gHja<_Hd$HtI%$aORo!a4P{v_L9^oKa==P7 zX0TPhN8}wTcU~z-yA~XPKPqEA^Z1HV8D707R5cI?Ksln3C=^Eupq80hL#m z?3fa@EV1)?1vH}>XymMt;;KGQlY6vWaMqrXHdbe9!4->X#B{UqCd7>CFBOsA>jRHgO^-ij-lGbKwL%}ZwbeX}UOYlCXKQC>`LYhGTdU$g=r zHCW(=fYmZP!mTb(lF^`*2>z+SO{y zkFM3%?5$AuXDag3G>;_PzD=voC!e*0zS_ca{w^D*vw$#P_Y$$a{;9`~XNB$O`9Am8 zaKIdz;9rk4-^Rjqqq6ZN#6O5n&Ck_#;}!lkw(UhUDs3?cZe+(j~#V_c^Yy z@RGM7Z4-5e`;cOi#mF;lXC&;HMk!x~r2bvVU*7^#TE;8wCG47Y+K8;q3OFN?_TXIS z;RQ#hIw)YP#5)X9u{HQPqg0$6kZmd~JVU>seFrNh!FiQHc}5<&uT@cJd~YX(h)Yy*DcfIJ31n^UQbd=dSL{gyOwr5dI( z24$-xy}c_*kMB$7=7!BK9uj38&&1P@l4w{yQQ4?8Rw~0Gl ztJfZ&_5k<39gT|qABCE7Ncn_|?e^v~fzCR%NZm`#2Pm4S+38u+#^Rm zGU5Gm5|x6~CoaB35e_CI{#WxpWnp8{UUYP|b~Hu5)mKMTi1QlM85(epsNvdOF~n3I zwjp3uynx=R|4CLa_Bwrc-!q%2k9*B-ue0Agu&~?g47~QtLdfZP-Arc5$7zAjPkT^! zYB5 z(V39>JahA_Y2(A;*^ejo(b?(IpX|x#^xeCYk-Tesa&~ewu}6pJ?@m??5|PwbLc{Vj z-#cn>YQzK^V&f+_3g1+-d@BKp7R7hh!u|fhdNej0+ztSkMe`3?+yf@Az>1hqo7pqhot=VXrQZN2h0J#}^;Z#i=3Jh22KjJ1=&v34#qy zQ=MK+u$N%w=Kv&PM>rpF1V1HvD||555@v{hNN6bhvu#@YO%oI_m{|Q`r#rOFR(sI@ zv_6puB^?M;bX{$n&~jJp&crl_=3r>`Th@WO)|JZD$GJI-kA(X&O&c3|x|E7HW<;~o zIwA)PWVtGyso+P3hfgWS)C2j!kV@s%#_d(u3Qg3n>y1g<6<`)kX)JP-Ya^a%u5CiV zNF!Lo{lRd*)9Rbnr(Zt7yUGDi@$=tufE?<7B?lZh`>xlUwVS5n44U18gMJgW!MHs% yK-Qq`b-HGMYYuqYgAe3@|3_?E2yD89?6M1JT3>llch(z|Ui}y8AnvUGR{#K}ArO`T literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/dashboard/drilldowns/mappings.json b/x-pack/test/functional/es_archives/dashboard/drilldowns/mappings.json new file mode 100644 index 0000000000000..210fade40c648 --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/drilldowns/mappings.json @@ -0,0 +1,244 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/services/dashboard/drilldowns_manage.ts b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts new file mode 100644 index 0000000000000..4d1b856b7c38a --- /dev/null +++ b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts @@ -0,0 +1,97 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +const CREATE_DRILLDOWN_FLYOUT_DATA_TEST_SUBJ = 'dashboardCreateDrilldownFlyout'; +const MANAGE_DRILLDOWNS_FLYOUT_DATA_TEST_SUBJ = 'dashboardEditDrilldownFlyout'; +const DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM = + 'actionFactoryItem-DASHBOARD_TO_DASHBOARD_DRILLDOWN'; +const DASHBOARD_TO_DASHBOARD_ACTION_WIZARD = + 'selectedActionFactory-DASHBOARD_TO_DASHBOARD_DRILLDOWN'; +const DESTINATION_DASHBOARD_SELECT = 'dashboardDrilldownSelectDashboard'; +const DRILLDOWN_WIZARD_SUBMIT = 'drilldownWizardSubmit'; + +export function DashboardDrilldownsManageProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const flyout = getService('flyout'); + const comboBox = getService('comboBox'); + + return new (class DashboardDrilldownsManage { + async expectsCreateDrilldownFlyoutOpen() { + log.debug('expectsCreateDrilldownFlyoutOpen'); + await testSubjects.existOrFail(CREATE_DRILLDOWN_FLYOUT_DATA_TEST_SUBJ); + } + + async expectsManageDrilldownsFlyoutOpen() { + log.debug('expectsManageDrilldownsFlyoutOpen'); + await testSubjects.existOrFail(MANAGE_DRILLDOWNS_FLYOUT_DATA_TEST_SUBJ); + } + + async expectsCreateDrilldownFlyoutClose() { + log.debug('expectsCreateDrilldownFlyoutClose'); + await testSubjects.missingOrFail(CREATE_DRILLDOWN_FLYOUT_DATA_TEST_SUBJ); + } + + async expectsManageDrilldownsFlyoutClose() { + log.debug('expectsManageDrilldownsFlyoutClose'); + await testSubjects.missingOrFail(MANAGE_DRILLDOWNS_FLYOUT_DATA_TEST_SUBJ); + } + + async fillInDashboardToDashboardDrilldownWizard({ + drilldownName, + destinationDashboardTitle, + }: { + drilldownName: string; + destinationDashboardTitle: string; + }) { + await this.fillInDrilldownName(drilldownName); + await this.selectDashboardToDashboardActionIfNeeded(); + await this.selectDestinationDashboard(destinationDashboardTitle); + } + + async fillInDrilldownName(name: string) { + await testSubjects.setValue('drilldownNameInput', name); + } + + async selectDashboardToDashboardActionIfNeeded() { + if (await testSubjects.exists(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM)) { + await testSubjects.click(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM); + } + await testSubjects.existOrFail(DASHBOARD_TO_DASHBOARD_ACTION_WIZARD); + } + + async selectDestinationDashboard(title: string) { + await comboBox.set(DESTINATION_DASHBOARD_SELECT, title); + } + + async saveChanges() { + await testSubjects.click(DRILLDOWN_WIZARD_SUBMIT); + } + + async deleteDrilldownsByTitles(titles: string[]) { + const drilldowns = await testSubjects.findAll('listManageDrilldownsItem'); + + for (const drilldown of drilldowns) { + const nameColumn = await drilldown.findByCssSelector( + '[data-test-subj="drilldownListItemName"]' + ); + const name = await nameColumn.getVisibleText(); + if (titles.includes(name)) { + const checkbox = await drilldown.findByTagName('input'); + await checkbox.click(); + } + } + const deleteBtn = await testSubjects.find('listManageDeleteDrilldowns'); + await deleteBtn.click(); + } + + async closeFlyout() { + await flyout.ensureAllClosed(); + } + })(); +} diff --git a/x-pack/test/functional/services/dashboard/index.ts b/x-pack/test/functional/services/dashboard/index.ts new file mode 100644 index 0000000000000..dee525fa0a388 --- /dev/null +++ b/x-pack/test/functional/services/dashboard/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DashboardDrilldownPanelActionsProvider } from './panel_drilldown_actions'; +export { DashboardDrilldownsManageProvider } from './drilldowns_manage'; diff --git a/x-pack/test/functional/services/dashboard/panel_drilldown_actions.ts b/x-pack/test/functional/services/dashboard/panel_drilldown_actions.ts new file mode 100644 index 0000000000000..52f7540f9f2f7 --- /dev/null +++ b/x-pack/test/functional/services/dashboard/panel_drilldown_actions.ts @@ -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. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +const CREATE_DRILLDOWN_DATA_TEST_SUBJ = 'embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'; +const MANAGE_DRILLDOWNS_DATA_TEST_SUBJ = 'embeddablePanelAction-OPEN_FLYOUT_EDIT_DRILLDOWN'; + +export function DashboardDrilldownPanelActionsProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const testSubjects = getService('testSubjects'); + + return new (class DashboardDrilldownPanelActions { + async expectExistsCreateDrilldownAction() { + log.debug('expectExistsCreateDrilldownAction'); + await testSubjects.existOrFail(CREATE_DRILLDOWN_DATA_TEST_SUBJ); + } + + async expectMissingCreateDrilldwonAction() { + log.debug('expectMissingCreateDrilldownAction'); + await testSubjects.existOrFail(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ); + } + + async clickCreateDrilldown() { + log.debug('clickCreateDrilldown'); + await this.expectExistsCreateDrilldownAction(); + await testSubjects.clickWhenNotDisabled(CREATE_DRILLDOWN_DATA_TEST_SUBJ); + } + + async expectExistsManageDrilldownsAction() { + log.debug('expectExistsCreateDrilldownAction'); + await testSubjects.existOrFail(CREATE_DRILLDOWN_DATA_TEST_SUBJ); + } + + async expectMissingManageDrilldownsAction() { + log.debug('expectExistsRemovePanelAction'); + await testSubjects.existOrFail(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ); + } + + async clickManageDrilldowns() { + log.debug('clickManageDrilldowns'); + await this.expectExistsManageDrilldownsAction(); + await testSubjects.clickWhenNotDisabled(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ); + } + + async expectMultipleActionsMenuOpened() { + log.debug('exceptMultipleActionsMenuOpened'); + await testSubjects.existOrFail('multipleActionsContextMenu'); + } + + async clickActionByText(text: string) { + log.debug(`clickActionByText: "${text}"`); + const menu = await testSubjects.find('multipleActionsContextMenu'); + const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); + for (const item of items) { + const currentText = await item.getVisibleText(); + if (currentText === text) { + return await item.click(); + } + } + + throw new Error(`No action matching text "${text}"`); + } + })(); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index aec91ba9e9034..f1d84f3054aa0 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -49,6 +49,10 @@ import { InfraSourceConfigurationFormProvider } from './infra_source_configurati import { LogsUiProvider } from './logs_ui'; import { MachineLearningProvider } from './ml'; import { TransformProvider } from './transform'; +import { + DashboardDrilldownPanelActionsProvider, + DashboardDrilldownsManageProvider, +} from './dashboard'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -91,4 +95,6 @@ export const services = { logsUi: LogsUiProvider, ml: MachineLearningProvider, transform: TransformProvider, + dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, + dashboardDrilldownsManage: DashboardDrilldownsManageProvider, }; From 5ea78184ad63bda26dc93c1ffd0eb5ab053f100a Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Sat, 21 Mar 2020 10:00:35 -0500 Subject: [PATCH 17/32] documentation update --- ...ta-public.datapublicpluginstart.actions.md | 2 + ...ugins-data-public.datapublicpluginstart.md | 2 +- ...na-plugin-plugins-data-public.esfilters.md | 1 + src/plugins/data/public/public.api.md | 107 ++++++++++-------- 4 files changed, 61 insertions(+), 51 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md index 3e966caa30799..1476e6b574d8e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md @@ -9,5 +9,7 @@ ```typescript actions: { createFiltersFromEvent: typeof createFiltersFromEvent; + valueClickActionGetFilters: typeof valueClickActionGetFilters; + selectRangeActionGetFilters: typeof selectRangeActionGetFilters; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md index a623e91388fd6..7c5e65bc1708e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md @@ -14,7 +14,7 @@ export interface DataPublicPluginStart | Property | Type | Description | | --- | --- | --- | -| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromEvent: typeof createFiltersFromEvent;
} | | +| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromEvent: typeof createFiltersFromEvent;
valueClickActionGetFilters: typeof valueClickActionGetFilters;
selectRangeActionGetFilters: typeof selectRangeActionGetFilters;
} | | | [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | | | [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | | | [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 7fd65e5db35f3..37142cf1794c3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -49,6 +49,7 @@ esFilters: { generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; + convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index dad3a8e639bc5..270e21820a52a 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -39,6 +39,7 @@ import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PopoverAnchorPosition } from '@elastic/eui'; import { PublicUiSettingsParams } from 'src/core/server/types'; +import { RangeFilter as RangeFilter_2 } from 'src/plugins/data/public'; import React from 'react'; import * as React_2 from 'react'; import { Required } from '@kbn/utility-types'; @@ -225,6 +226,8 @@ export interface DataPublicPluginStart { // (undocumented) actions: { createFiltersFromEvent: typeof createFiltersFromEvent; + valueClickActionGetFilters: typeof valueClickActionGetFilters; + selectRangeActionGetFilters: typeof selectRangeActionGetFilters; }; // Warning: (ae-forgotten-export) The symbol "AutocompleteStart" needs to be exported by the entry point index.d.ts // @@ -367,6 +370,7 @@ export const esFilters: { generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; + convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; }; @@ -1795,58 +1799,61 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/common/es_query/filters/match_all_filter.ts:28:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:60:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:56:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:57:5 - (ae-forgotten-export) The symbol "valueClickActionGetFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:58:5 - (ae-forgotten-export) The symbol "selectRangeActionGetFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:66:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) From 100c4f090dac36a9388579dba45b6f021c7700b0 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Mar 2020 11:49:12 +0100 Subject: [PATCH 18/32] chore clean-ups fix type --- .../filters/create_filters_from_event.ts | 5 ---- .../public/actions/select_range_action.ts | 10 +++---- .../data/public/actions/value_click_action.ts | 15 +++++----- .../dashboard_drilldowns_services.ts | 2 -- .../collect_config.tsx | 29 +++++++++++++------ .../drilldown.tsx | 1 - 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.ts b/src/plugins/data/public/actions/filters/create_filters_from_event.ts index 8b6135a246ce1..e62945a592072 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_event.ts @@ -84,11 +84,6 @@ const createFilter = async (table: EventData['table'], columnIndex: number, rowI if (!column.meta || !column.meta.indexPatternId) { return; } - console.log('aggConfig', column.meta.indexPatternId); // eslint-disable-line - console.log( // eslint-disable-line - 'This fails, but not always', - await getIndexPatterns().get(column.meta.indexPatternId) - ); const aggConfig = deserializeAggConfig({ type: column.meta.type, aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index c5358285acf1b..e17d336a852d1 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -34,11 +34,11 @@ export interface SelectRangeActionContext { } async function isCompatible(context: SelectRangeActionContext) { - // try { - return Boolean(await onBrushEvent(context.data)); - // } catch { - // return false; - // } + try { + return Boolean(await onBrushEvent(context.data)); + } catch { + return false; + } } export const selectRangeActionGetFilters = async ({ diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index d2f7d7cc0272f..90549f1e9e292 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -38,13 +38,14 @@ export interface ValueClickActionContext { } async function isCompatible(context: ValueClickActionContext) { - // try { - const filters: Filter[] = - (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || []; - return filters.length > 0; - // } catch { - // return false; - // } + try { + const filters: Filter[] = + (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || + []; + return filters.length > 0; + } catch { + return false; + } } // this allows the user to select which filter to use diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index dd887130d4b5c..71502a7dce026 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -31,9 +31,7 @@ interface BootstrapParams { } export class DashboardDrilldownsService { - // async bootstrap(core: CoreSetup, plugins: SetupDependencies) { bootstrap( - // core: CoreSetup<{ drilldowns: DrilldownsStart }>, core: CoreSetup, plugins: SetupDependencies, { enableDrilldowns }: BootstrapParams diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx index 0cc2d97b8ee56..572bc1c04b2ad 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx @@ -43,6 +43,7 @@ export class CollectConfigContainer extends React.Component< CollectConfigProps, CollectConfigContainerState > { + private isMounted = true; state = { dashboards: [], isLoading: false, @@ -51,11 +52,20 @@ export class CollectConfigContainer extends React.Component< delay: false, }; + constructor(props: CollectConfigProps) { + super(props); + this.debouncedLoadDashboards = debounce(this.loadDashboards.bind(this), 500); + } + componentDidMount() { this.loadSelectedDashboard(); this.loadDashboards(); } + componentWillUnmount() { + this.isMounted = false; + } + loadSelectedDashboard() { const { config } = this.props; this.props.deps.getSavedObjectsClient().then(savedObjectsClient => { @@ -63,15 +73,16 @@ export class CollectConfigContainer extends React.Component< savedObjectsClient .get<{ title: string }>('dashboard', config.dashboardId) .then(dashboard => { + if (!this.isMounted) return; this.setState({ selectedDashboard: dashboardSavedObjectToMenuItem(dashboard) }); }); } }); } + private readonly debouncedLoadDashboards: (searchString?: string) => void; loadDashboards(searchString?: string) { - // const currentDashboard = this.props.context.placeContext.embeddable.parent; - // const currentDashboardId = currentDashboard && currentDashboard.id; + const currentDashboardId = this.props.context.placeContext.embeddable?.parent?.id; this.setState({ searchString, isLoading: true }); this.props.deps.getSavedObjectsClient().then(savedObjectsClient => { savedObjectsClient @@ -83,12 +94,12 @@ export class CollectConfigContainer extends React.Component< perPage: 100, }) .then(({ savedObjects }) => { - if (searchString === this.state.searchString) { - const dashboardList = savedObjects.map(dashboardSavedObjectToMenuItem); - // temporarily disable for dev purposes - // .filter(({ value }) => value !== currentDashboardId); - this.setState({ dashboards: dashboardList, isLoading: false }); - } + if (!this.isMounted) return; + if (searchString !== this.state.searchString) return; + const dashboardList = savedObjects + .map(dashboardSavedObjectToMenuItem) + .filter(({ value }) => !currentDashboardId || value !== currentDashboardId); + this.setState({ dashboards: dashboardList, isLoading: false }); }); }); } @@ -107,7 +118,7 @@ export class CollectConfigContainer extends React.Component< onDashboardSelect={dashboardId => { onConfig({ ...config, dashboardId }); }} - onSearchChange={debounce(() => this.loadDashboards(), 500)} + onSearchChange={this.debouncedLoadDashboards} onCurrentFiltersToggle={() => onConfig({ ...config, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 4ae76527bc083..3e95c86a4a0ee 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -82,7 +82,6 @@ export class DashboardToDashboardDrilldown // for brush event this time range would be overwritten let timeRange = config.useCurrentDateRange ? currentTimeRange : undefined; - // @ts-ignore TODO if (context.data.range) { // look up by range const { restOfFilters, timeRangeFilter } = From 340fd890d6cbd12593ca4108cd05cd0239605a51 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Mar 2020 14:28:03 +0100 Subject: [PATCH 19/32] basic unit test for dashboard drilldown --- .../drilldown.test.tsx | 310 +++++++++++++++++- 1 file changed, 301 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index 0fb60bb1064a1..08a150cb78088 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -4,17 +4,309 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DashboardToDashboardDrilldown } from './drilldown'; +import { UrlGeneratorContract } from '../../../../../../../src/plugins/share/public'; +import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { ActionContext, Config } from './types'; +import { VisualizeEmbeddableContract } from '../../../../../../../src/legacy/core_plugins/visualizations/public/'; +import { + Filter, + FilterStateStore, + Query, + RangeFilter, + TimeRange, +} from '../../../../../../../src/plugins/data/common'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; + +// convenient to use real implementation here. +import { createDirectAccessDashboardLinkGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator'; + describe('.isConfigValid()', () => { - test.todo('returns false for incorrect config'); - test.todo('returns true for incorrect config'); + const drilldown = new DashboardToDashboardDrilldown({} as any); + + test('returns false for invalid config with missing dashboard id', () => { + expect( + drilldown.isConfigValid({ + dashboardId: '', + useCurrentDateRange: false, + useCurrentFilters: false, + }) + ).toBe(false); + }); + + test('returns true for valid config', () => { + expect( + drilldown.isConfigValid({ + dashboardId: 'id', + useCurrentDateRange: false, + useCurrentFilters: false, + }) + ).toBe(true); + }); +}); + +test('config component exist', () => { + const drilldown = new DashboardToDashboardDrilldown({} as any); + expect(drilldown.CollectConfig).toEqual(expect.any(Function)); }); describe('.execute()', () => { - test.todo('navigates to correct dashboard'); - test.todo( - 'when user chooses to keep current filters, current fileters are set on destination dashboard' - ); - test.todo( - 'when user chooses to keep current time range, current time range is set on destination dashboard' - ); + /** + * A convenience test setup helper + * Beware: `dataPluginMock.createStartContract().actions` and extracting filters from event is mocked! + * The url generation is not mocked and uses real implementation + * So this tests are mostly focused on making sure the filters returned from `dataPluginMock.createStartContract().actions` helpers + * end up in resulting navigation path + */ + async function setupTestBed( + config: Partial, + embeddableInput: { filters?: Filter[]; timeRange?: TimeRange; query?: Query }, + filtersFromEvent: { restOfFilters?: Filter[]; timeRangeFilter?: RangeFilter }, + useRangeEvent = false + ) { + const navigateToApp = jest.fn(); + const dataPluginActions = dataPluginMock.createStartContract().actions; + const drilldown = new DashboardToDashboardDrilldown({ + getNavigateToApp: () => Promise.resolve(navigateToApp), + getGetUrlGenerator: () => + Promise.resolve( + () => + createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ appBasePath: 'test', useHashedUrl: false }) + ) as UrlGeneratorContract + ), + getDataPluginActions: () => Promise.resolve(dataPluginActions), + getSavedObjectsClient: () => + Promise.resolve(savedObjectsServiceMock.createStartContract().client), + }); + const selectRangeFiltersSpy = jest + .spyOn(dataPluginActions, 'selectRangeActionGetFilters') + .mockImplementationOnce(() => + Promise.resolve({ + restOfFilters: filtersFromEvent.restOfFilters || [], + timeRangeFilter: filtersFromEvent.timeRangeFilter, + }) + ); + const valueClickFiltersSpy = jest + .spyOn(dataPluginActions, 'valueClickActionGetFilters') + .mockImplementationOnce(() => + Promise.resolve({ + restOfFilters: filtersFromEvent.restOfFilters || [], + timeRangeFilter: filtersFromEvent.timeRangeFilter, + }) + ); + + await drilldown.execute( + { + dashboardId: 'id', + useCurrentFilters: false, + useCurrentDateRange: false, + ...config, + }, + ({ + data: { + range: useRangeEvent ? {} : undefined, + }, + embeddable: { + getInput: () => ({ + filters: [], + timeRange: { from: 'now-15m', to: 'now' }, + query: { query: 'test', language: 'kuery' }, + ...embeddableInput, + }), + }, + } as unknown) as ActionContext + ); + + if (useRangeEvent) { + expect(selectRangeFiltersSpy).toBeCalledTimes(1); + expect(valueClickFiltersSpy).toBeCalledTimes(0); + } else { + expect(selectRangeFiltersSpy).toBeCalledTimes(0); + expect(valueClickFiltersSpy).toBeCalledTimes(1); + } + + expect(navigateToApp).toBeCalledTimes(1); + expect(navigateToApp.mock.calls[0][0]).toBe('kibana'); + + return { + navigatedPath: navigateToApp.mock.calls[0][1]?.path, + }; + } + + test('navigates to correct dashboard', async () => { + const testDashboardId = 'dashboardId'; + const { navigatedPath } = await setupTestBed( + { + dashboardId: testDashboardId, + }, + {}, + {}, + false + ); + + expect(navigatedPath).toEqual(expect.stringContaining(`dashboard/${testDashboardId}`)); + }); + + test('navigates with query', async () => { + const queryString = 'querystring'; + const queryLanguage = 'kuery'; + const { navigatedPath } = await setupTestBed( + {}, + { + query: { query: queryString, language: queryLanguage }, + }, + {}, + true + ); + + expect(navigatedPath).toEqual(expect.stringContaining(queryString)); + expect(navigatedPath).toEqual(expect.stringContaining(queryLanguage)); + }); + + test('when user chooses to keep current filters, current filters are set on destination dashboard', async () => { + const existingAppFilterKey = 'appExistingFilter'; + const existingGlobalFilterKey = 'existingGlobalFilter'; + const newAppliedFilterKey = 'newAppliedFilter'; + + const { navigatedPath } = await setupTestBed( + { + useCurrentFilters: true, + }, + { + filters: [getFilter(false, existingAppFilterKey), getFilter(true, existingGlobalFilterKey)], + }, + { + restOfFilters: [getFilter(false, newAppliedFilterKey)], + }, + false + ); + + expect(navigatedPath).toEqual(expect.stringContaining(existingAppFilterKey)); + expect(navigatedPath).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(navigatedPath).toEqual(expect.stringContaining(newAppliedFilterKey)); + }); + + test('when user chooses to remove current filters, current app filters are remove on destination dashboard', async () => { + const existingAppFilterKey = 'appExistingFilter'; + const existingGlobalFilterKey = 'existingGlobalFilter'; + const newAppliedFilterKey = 'newAppliedFilter'; + + const { navigatedPath } = await setupTestBed( + { + useCurrentFilters: false, + }, + { + filters: [getFilter(false, existingAppFilterKey), getFilter(true, existingGlobalFilterKey)], + }, + { + restOfFilters: [getFilter(false, newAppliedFilterKey)], + }, + false + ); + + expect(navigatedPath).not.toEqual(expect.stringContaining(existingAppFilterKey)); + expect(navigatedPath).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(navigatedPath).toEqual(expect.stringContaining(newAppliedFilterKey)); + }); + + test('when user chooses to keep current time range, current time range is passed in url', async () => { + const { navigatedPath } = await setupTestBed( + { + useCurrentDateRange: true, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + {}, + false + ); + + expect(navigatedPath).toEqual(expect.stringContaining('now-300m')); + }); + + test('when user chooses to not keep current time range, no current time range is passed in url', async () => { + const { navigatedPath } = await setupTestBed( + { + useCurrentDateRange: false, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + {}, + false + ); + + expect(navigatedPath).not.toEqual(expect.stringContaining('now-300m')); + }); + + test('if range filter contains date, then it is passed as time', async () => { + const { navigatedPath } = await setupTestBed( + { + useCurrentDateRange: true, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + { timeRangeFilter: getMockTimeRangeFilter() }, + true + ); + + expect(navigatedPath).not.toEqual(expect.stringContaining('now-300m')); + expect(navigatedPath).toEqual(expect.stringContaining('2020-03-23')); + }); }); + +function getFilter(isPinned: boolean, queryKey: string): Filter { + return { + $state: { + store: isPinned ? esFilters.FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, + }, + meta: { + index: 'logstash-*', + disabled: false, + negate: false, + alias: null, + }, + query: { + match: { + [queryKey]: 'any', + }, + }, + }; +} + +function getMockTimeRangeFilter(): RangeFilter { + return { + meta: { + index: 'logstash-*', + params: { + gte: '2020-03-23T13:10:29.665Z', + lt: '2020-03-23T13:10:36.736Z', + format: 'strict_date_optional_time', + }, + type: 'range', + key: 'order_date', + disabled: false, + negate: false, + alias: null, + }, + range: { + order_date: { + gte: '2020-03-23T13:10:29.665Z', + lt: '2020-03-23T13:10:36.736Z', + format: 'strict_date_optional_time', + }, + }, + }; +} From 406facde4c6e0d9726e1760f8b9986e98d0ca1c5 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Mar 2020 15:46:51 +0100 Subject: [PATCH 20/32] remove test todos decided to skip those tests because not clear how to test due to EuiCombobox is using react-virtualized and options list is not rendered in jsdom env --- .../dashboard_drilldown_config.test.tsx | 11 ----------- .../collect_config.test.tsx | 9 --------- 2 files changed, 20 deletions(-) delete mode 100644 x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx delete mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.test.tsx diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx b/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx deleted file mode 100644 index 911ff6f632635..0000000000000 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx +++ /dev/null @@ -1,11 +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. - */ - -test.todo('renders list of dashboards'); -test.todo('renders correct selected dashboard'); -test.todo('can change dashboard'); -test.todo('can toggle "use current filters" switch'); -test.todo('can toggle "date range" switch'); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.test.tsx deleted file mode 100644 index 95101605ce468..0000000000000 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.test.tsx +++ /dev/null @@ -1,9 +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. - */ - -test.todo('displays all dashboard in a list'); -test.todo('does not display dashboard on which drilldown is being created'); -test.todo('updates config object correctly'); From bf9e50548c20de1b11423cd07353839d87fad5b4 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Mar 2020 16:07:07 +0100 Subject: [PATCH 21/32] remove config --- config/kibana.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/kibana.yml b/config/kibana.yml index d387f7468a972..0780841ca057e 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -110,6 +110,3 @@ # Specifies locale to be used for all localizable strings, dates and number formats. # Supported languages are the following: English - en , by default , Chinese - zh-CN . #i18n.locale: "en" - -# Enables "Drilldowns" functionality on dashboard. Set to true by default. -# xpack.dashboardEnhanced.drilldowns.enabled: false From d7633b3e28c2c8b256005d5e39a896d5a3b9f5d2 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Mar 2020 17:23:23 +0100 Subject: [PATCH 22/32] improve back button with filter comparison tweak --- .../filter_manager/compare_filters.test.ts | 22 +++++++++++++++++++ .../query/filter_manager/compare_filters.ts | 6 +++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts index b0bb2f754d6cf..c42bd720d3b7f 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts @@ -92,6 +92,28 @@ describe('filter manager utilities', () => { expect(compareFilters(f1, f2)).toBeTruthy(); }); + test('should compare duplicates, assuming $state: { store: FilterStateStore.APP_STATE } is default', () => { + const f1 = { + $state: { store: FilterStateStore.APP_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + const f2 = { + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + const f3 = { + $state: { store: FilterStateStore.GLOBAL_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + + // don't take state into account at all: + expect(compareFilters(f1, f2)).toBeTruthy(); + expect(compareFilters(f2, f3)).toBeTruthy(); + + // compare with state: + expect(compareFilters(f1, f2, { state: true })).toBeTruthy(); + expect(compareFilters(f2, f3, { state: true })).toBeFalsy(); + }); + test('should compare filters array to non array', () => { const f1 = buildQueryFilter( { _type: { match: { query: 'apache', type: 'phrase' } } }, diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts index e047d5e0665d5..c19d00faaf251 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -18,7 +18,7 @@ */ import { defaults, isEqual, omit, map } from 'lodash'; -import { FilterMeta, Filter } from '../../es_query'; +import { FilterMeta, Filter, FilterStateStore } from '../../es_query'; export interface FilterCompareOptions { disabled?: boolean; @@ -42,11 +42,13 @@ const mapFilter = ( comparators: FilterCompareOptions, excludedAttributes: string[] ) => { - const cleaned: FilterMeta = omit(filter, excludedAttributes); + const cleaned: Filter & FilterMeta = omit(filter, excludedAttributes); if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); if (comparators.disabled) cleaned.alias = filter.meta?.alias; + if (comparators.state) + cleaned.$state = { store: filter.$state?.store ?? FilterStateStore.APP_STATE }; return cleaned; }; From 59101ef1ea3e1e89bd8226c5e1f555f7f5b413c3 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Mar 2020 17:29:28 +0100 Subject: [PATCH 23/32] dashboard filters/date option off by default --- .../drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 3e95c86a4a0ee..1425c6033578c 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -45,8 +45,8 @@ export class DashboardToDashboardDrilldown public readonly createConfig = () => ({ dashboardId: '', - useCurrentFilters: true, - useCurrentDateRange: true, + useCurrentFilters: false, + useCurrentDateRange: false, }); public readonly isConfigValid = (config: Config): config is Config => { From bb308d6858f61f3706d8deea8c8889fe6040387c Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Mon, 23 Mar 2020 12:08:00 -0500 Subject: [PATCH 24/32] revert change to config/kibana.yml --- config/kibana.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/kibana.yml b/config/kibana.yml index 13728a7bf212a..0780841ca057e 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -110,6 +110,3 @@ # Specifies locale to be used for all localizable strings, dates and number formats. # Supported languages are the following: English - en , by default , Chinese - zh-CN . #i18n.locale: "en" - -# Enables "Drilldowns" functionality on dashboard. Set to false by default. -xpack.dashboardEnhanced.drilldowns.enabled: true From 31e28a3eb965eb1330bd9a4bf9f544781ffb757f Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Mon, 23 Mar 2020 13:27:23 -0500 Subject: [PATCH 25/32] remove unneeded comments --- src/plugins/data/public/actions/value_click_action.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 90549f1e9e292..ae1483c99bcd1 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -90,11 +90,9 @@ export const valueClickActionGetFilters = async ( throw new IncompatibleActionError(); } - // const filters: Filter[] = (await createFiltersFromEvent(data.data || [data], data.negate)) || []; let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); - // filters if (selectedFilters.length > 1) { selectedFilters = await filterSelection(filters); From 38ec7a03a2b09794bcfb2680cb511b82c5f267dd Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Mon, 23 Mar 2020 21:09:33 -0500 Subject: [PATCH 26/32] use default time range as appropriate --- .../drilldown.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 1425c6033578c..fd88213bff6b3 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -8,7 +8,10 @@ import React from 'react'; import { CoreStart } from 'src/core/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; -import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; +import { + DASHBOARD_APP_URL_GENERATOR, + DashboardContainerInput, +} from '../../../../../../../src/plugins/dashboard/public'; import { VisualizeEmbeddableContract } from '../../../../../../../src/legacy/core_plugins/visualizations/public'; import { PlaceContext, ActionContext, Config, CollectConfigProps } from './types'; @@ -60,6 +63,8 @@ export class DashboardToDashboardDrilldown ) => { const getUrlGenerator = await this.params.getGetUrlGenerator(); const navigateToApp = await this.params.getNavigateToApp(); + const savedObjectsClient = await this.params.getSavedObjectsClient(); + const { selectRangeActionGetFilters, valueClickActionGetFilters, @@ -70,6 +75,16 @@ export class DashboardToDashboardDrilldown filters: currentFilters, } = context.embeddable.getInput(); + const savedDashboard = await savedObjectsClient.get<{ timeTo: string; timeFrom: string }>( + 'dashboard', + config.dashboardId as string + ); + + const defaultTimeRange = { + to: savedDashboard.attributes.timeTo, + from: savedDashboard.attributes.timeFrom, + }; + // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) // otherwise preserve only pinned const filters = @@ -80,7 +95,7 @@ export class DashboardToDashboardDrilldown // if useCurrentDashboardDataRange is enabled, then preserve current time range // if undefined is passed, then destination dashboard will figure out time range itself // for brush event this time range would be overwritten - let timeRange = config.useCurrentDateRange ? currentTimeRange : undefined; + let timeRange = config.useCurrentDateRange ? currentTimeRange : defaultTimeRange; if (context.data.range) { // look up by range From 4a8d4a1eb8c7b0dd676f97453f63c09b459e09a7 Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Mon, 23 Mar 2020 22:22:20 -0500 Subject: [PATCH 27/32] fix type, add filter icon, add text --- src/plugins/data/public/actions/apply_filter_action.ts | 1 + src/plugins/data/public/actions/select_range_action.ts | 1 + src/plugins/data/public/actions/value_click_action.ts | 1 + .../dashboard_to_dashboard_drilldown/drilldown.tsx | 5 +---- .../components/connected_flyout_manage_drilldowns/i18n.ts | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index bd20c6f632a3a..ebaac6b745bec 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -42,6 +42,7 @@ export function createFilterAction( return createAction({ type: ACTION_GLOBAL_APPLY_FILTER, id: ACTION_GLOBAL_APPLY_FILTER, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index e17d336a852d1..11fb287c9ae2c 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -80,6 +80,7 @@ export function selectRangeAction( return createAction({ type: ACTION_SELECT_RANGE, id: ACTION_SELECT_RANGE, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index ae1483c99bcd1..d51b604a0c392 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -128,6 +128,7 @@ export function valueClickAction( return createAction({ type: ACTION_VALUE_CLICK, id: ACTION_VALUE_CLICK, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index fd88213bff6b3..d78959789e4a2 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -8,10 +8,7 @@ import React from 'react'; import { CoreStart } from 'src/core/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; -import { - DASHBOARD_APP_URL_GENERATOR, - DashboardContainerInput, -} from '../../../../../../../src/plugins/dashboard/public'; +import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; import { VisualizeEmbeddableContract } from '../../../../../../../src/legacy/core_plugins/visualizations/public'; import { PlaceContext, ActionContext, Config, CollectConfigProps } from './types'; diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts index 70f4d735e2a74..9fa366445ebae 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts @@ -15,7 +15,7 @@ export const toastDrilldownCreated = { ), text: (drilldownName: string) => i18n.translate('xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText', { - defaultMessage: 'You created "{drilldownName}"', + defaultMessage: 'You created "{drilldownName}", save dashboard to test.', values: { drilldownName, }, @@ -31,7 +31,7 @@ export const toastDrilldownEdited = { ), text: (drilldownName: string) => i18n.translate('xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText', { - defaultMessage: 'You edited "{drilldownName}"', + defaultMessage: 'You edited "{drilldownName}", save dashboard to test.', values: { drilldownName, }, From 0deb6b4614db9749693cc5610e0feca201f7f344 Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Tue, 24 Mar 2020 00:13:45 -0500 Subject: [PATCH 28/32] fix test --- .../dashboard_to_dashboard_drilldown/drilldown.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index 08a150cb78088..bc73592501cd6 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -67,6 +67,11 @@ describe('.execute()', () => { ) { const navigateToApp = jest.fn(); const dataPluginActions = dataPluginMock.createStartContract().actions; + const savedObjectsClient = savedObjectsServiceMock.createStartContract().client; + savedObjectsClient.get = jest + .fn() + .mockReturnValue({ attributes: { from: 'now-15m', to: 'now' } }); + const drilldown = new DashboardToDashboardDrilldown({ getNavigateToApp: () => Promise.resolve(navigateToApp), getGetUrlGenerator: () => @@ -77,8 +82,7 @@ describe('.execute()', () => { ) as UrlGeneratorContract ), getDataPluginActions: () => Promise.resolve(dataPluginActions), - getSavedObjectsClient: () => - Promise.resolve(savedObjectsServiceMock.createStartContract().client), + getSavedObjectsClient: () => Promise.resolve(savedObjectsClient), }); const selectRangeFiltersSpy = jest .spyOn(dataPluginActions, 'selectRangeActionGetFilters') From 6a78486b8423348376d13fdaa26cba55fbf651cd Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 24 Mar 2020 16:25:28 +0100 Subject: [PATCH 29/32] change how time range is restored and improve back button for drilldowns --- .../np_ready/dashboard_app_controller.tsx | 21 +++++++++------ .../apps/management/_kibana_settings.js | 27 ++++++++++--------- .../drilldown.test.tsx | 3 --- .../drilldown.tsx | 13 +-------- 4 files changed, 29 insertions(+), 35 deletions(-) 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 d1e4c9d2d2a0c..e2236f2894c41 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 @@ -36,6 +36,7 @@ import { IndexPattern, IndexPatternsContract, Query, + QueryState, SavedQuery, syncQueryStateWithUrl, } from '../../../../../../plugins/data/public'; @@ -132,13 +133,6 @@ export class DashboardAppController { const queryFilter = filterManager; const timefilter = queryService.timefilter.timefilter; - // starts syncing `_g` portion of url with query services - // note: dashboard_state_manager.ts syncs `_a` portion of url - const { - stop: stopSyncingQueryServiceStateWithUrl, - hasInheritedQueryFromUrl: hasInheritedGlobalStateFromUrl, - } = syncQueryStateWithUrl(queryService, kbnUrlStateStorage); - let lastReloadRequestTime = 0; const dash = ($scope.dash = $route.current.locals.dash); if (dash.id) { @@ -170,9 +164,20 @@ export class DashboardAppController { // The hash check is so we only update the time filter on dashboard open, not during // normal cross app navigation. - if (dashboardStateManager.getIsTimeSavedWithDashboard() && !hasInheritedGlobalStateFromUrl) { + const initialGlobalStateInUrl = kbnUrlStateStorage.get('_g'); + const hasInheritedTimeRange = Boolean(initialGlobalStateInUrl?.time); + if (dashboardStateManager.getIsTimeSavedWithDashboard() && !hasInheritedTimeRange) { dashboardStateManager.syncTimefilterWithDashboard(timefilter); } + // starts syncing `_g` portion of url with query services + // note: dashboard_state_manager.ts syncs `_a` portion of url + // it is important to start this syncing after `dashboardStateManager.syncTimefilterWithDashboard(timefilter);` above is run, + // otherwise it will case redundant browser history record + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + queryService, + kbnUrlStateStorage + ); + $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; const getShouldShowEditHelp = () => diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index c99368ba4e859..97337d4573e2a 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -46,6 +46,18 @@ export default function({ getService, getPageObjects }) { }); describe('state:storeInSessionStorage', () => { + async function getStateFromUrl() { + const currentUrl = await browser.getCurrentUrl(); + let match = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); + if (match) return [match[2], match[3]]; + match = currentUrl.match(/(.*)?_a=(.*)&_g=(.*)/); + if (match) return [match[3], match[2]]; + + if (!match) { + throw new Error('State in url is missing or malformed'); + } + } + it('defaults to null', async () => { await PageObjects.settings.clickKibanaSettings(); const storeInSessionStorage = await PageObjects.settings.getAdvancedSettingCheckbox( @@ -58,10 +70,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used, // which is less than 20 characters. @@ -83,10 +92,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's less than the unhashed version, which will be // greater than 20 characters with the default state plus a time. @@ -100,10 +106,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.toggleAdvancedSettingCheckbox('state:storeInSessionStorage'); await PageObjects.header.clickDashboard(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used, // which is less than 20 characters. expect(globalState.length).to.be.greaterThan(20); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index bc73592501cd6..b368799e3a337 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -68,9 +68,6 @@ describe('.execute()', () => { const navigateToApp = jest.fn(); const dataPluginActions = dataPluginMock.createStartContract().actions; const savedObjectsClient = savedObjectsServiceMock.createStartContract().client; - savedObjectsClient.get = jest - .fn() - .mockReturnValue({ attributes: { from: 'now-15m', to: 'now' } }); const drilldown = new DashboardToDashboardDrilldown({ getNavigateToApp: () => Promise.resolve(navigateToApp), diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index d78959789e4a2..1404b0aa1f4dd 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -60,7 +60,6 @@ export class DashboardToDashboardDrilldown ) => { const getUrlGenerator = await this.params.getGetUrlGenerator(); const navigateToApp = await this.params.getNavigateToApp(); - const savedObjectsClient = await this.params.getSavedObjectsClient(); const { selectRangeActionGetFilters, @@ -72,16 +71,6 @@ export class DashboardToDashboardDrilldown filters: currentFilters, } = context.embeddable.getInput(); - const savedDashboard = await savedObjectsClient.get<{ timeTo: string; timeFrom: string }>( - 'dashboard', - config.dashboardId as string - ); - - const defaultTimeRange = { - to: savedDashboard.attributes.timeTo, - from: savedDashboard.attributes.timeFrom, - }; - // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) // otherwise preserve only pinned const filters = @@ -92,7 +81,7 @@ export class DashboardToDashboardDrilldown // if useCurrentDashboardDataRange is enabled, then preserve current time range // if undefined is passed, then destination dashboard will figure out time range itself // for brush event this time range would be overwritten - let timeRange = config.useCurrentDateRange ? currentTimeRange : defaultTimeRange; + let timeRange = config.useCurrentDateRange ? currentTimeRange : undefined; if (context.data.range) { // look up by range From 514561b364044ff1a24155dc1a20ba69a3948a5e Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 7 Apr 2020 17:54:53 +0200 Subject: [PATCH 30/32] resolve conflicts --- src/legacy/core_plugins/vis_type_vislib/public/plugin.ts | 2 +- src/plugins/embeddable/public/lib/triggers/triggers.ts | 6 +++--- .../dashboard_to_dashboard_drilldown/drilldown.test.tsx | 2 +- .../dashboard_to_dashboard_drilldown/drilldown.tsx | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index 2731fb6f5fbe6..4fbf891e15d8e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -111,6 +111,6 @@ export class VisTypeVislibPlugin implements Plugin { public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { setFormatService(data.fieldFormats); - setDataActions({ createFiltersFromEvent: data.actions.createFiltersFromEvent }); + setDataActions(data.actions); } } diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 7317b0c87c0b6..87b234a63aa9a 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -25,12 +25,12 @@ export interface EmbeddableContext { } export interface EmbeddableVisTriggerContext { - embeddable: T; - timeFieldName: string; + embeddable?: T; + timeFieldName?: string; data: { e?: MouseEvent; data: unknown; - range: unknown; + range?: unknown; }; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index b368799e3a337..ab88b58fe6b77 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -9,7 +9,6 @@ import { UrlGeneratorContract } from '../../../../../../../src/plugins/share/pub import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ActionContext, Config } from './types'; -import { VisualizeEmbeddableContract } from '../../../../../../../src/legacy/core_plugins/visualizations/public/'; import { Filter, FilterStateStore, @@ -21,6 +20,7 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; // convenient to use real implementation here. import { createDirectAccessDashboardLinkGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator'; +import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; describe('.isConfigValid()', () => { const drilldown = new DashboardToDashboardDrilldown({} as any); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 1404b0aa1f4dd..e42d310b33fcd 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -9,7 +9,6 @@ import { CoreStart } from 'src/core/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; -import { VisualizeEmbeddableContract } from '../../../../../../../src/legacy/core_plugins/visualizations/public'; import { PlaceContext, ActionContext, Config, CollectConfigProps } from './types'; import { CollectConfigContainer } from './collect_config'; @@ -17,6 +16,7 @@ import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; import { DrilldownDefinition as Drilldown } from '../../../../../drilldowns/public'; import { txtGoToDashboard } from './i18n'; import { DataPublicPluginStart, esFilters } from '../../../../../../../src/plugins/data/public'; +import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; export interface Params { getSavedObjectsClient: () => Promise; @@ -69,7 +69,7 @@ export class DashboardToDashboardDrilldown timeRange: currentTimeRange, query, filters: currentFilters, - } = context.embeddable.getInput(); + } = context.embeddable!.getInput(); // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) // otherwise preserve only pinned @@ -87,7 +87,7 @@ export class DashboardToDashboardDrilldown // look up by range const { restOfFilters, timeRangeFilter } = (await selectRangeActionGetFilters({ - timeFieldName: context.timeFieldName, + timeFieldName: context.timeFieldName!, data: context.data, })) || {}; filters.push(...(restOfFilters || [])); @@ -96,7 +96,7 @@ export class DashboardToDashboardDrilldown } } else { const { restOfFilters, timeRangeFilter } = await valueClickActionGetFilters({ - timeFieldName: context.timeFieldName, + timeFieldName: context.timeFieldName!, data: context.data, }); filters.push(...(restOfFilters || [])); From 40e396c6777227a3f1dae14ac08eedacc265a36d Mon Sep 17 00:00:00 2001 From: Matt Kime Date: Tue, 7 Apr 2020 21:14:48 -0500 Subject: [PATCH 31/32] fix async compile issue --- .../data/public/actions/value_click_action.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index d51b604a0c392..e346b3a6f0c3e 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -88,17 +88,20 @@ export const valueClickActionGetFilters = async ( }> => { if (!(await isCompatible({ timeFieldName, data }))) { throw new IncompatibleActionError(); - } + // note - the else statement is necessary due to odd compilation behavior + // code after the throw statement can't contain await statements + } else { + const filters: Filter[] = + (await createFiltersFromEvent(data.data || [data], data.negate)) || []; - const filters: Filter[] = (await createFiltersFromEvent(data.data || [data], data.negate)) || []; + let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); - let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); + if (selectedFilters.length > 1) { + selectedFilters = await filterSelection(filters); + } - if (selectedFilters.length > 1) { - selectedFilters = await filterSelection(filters); + return esFilters.extractTimeFilter(timeFieldName || '', selectedFilters); } - - return esFilters.extractTimeFilter(timeFieldName || '', selectedFilters); }; // gets and applies the filters From a9b86a96b9200863a86cc9beb4286e3db5862697 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 8 Apr 2020 11:25:08 +0200 Subject: [PATCH 32/32] remove redundant test --- .../filter_manager/compare_filters.test.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts index 3faa47a5000e0..0c3947ade8221 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts @@ -92,28 +92,6 @@ describe('filter manager utilities', () => { expect(compareFilters(f1, f2)).toBeTruthy(); }); - test('should compare duplicates, assuming $state: { store: FilterStateStore.APP_STATE } is default', () => { - const f1 = { - $state: { store: FilterStateStore.APP_STATE }, - ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), - }; - const f2 = { - ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), - }; - const f3 = { - $state: { store: FilterStateStore.GLOBAL_STATE }, - ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), - }; - - // don't take state into account at all: - expect(compareFilters(f1, f2)).toBeTruthy(); - expect(compareFilters(f2, f3)).toBeTruthy(); - - // compare with state: - expect(compareFilters(f1, f2, { state: true })).toBeTruthy(); - expect(compareFilters(f2, f3, { state: true })).toBeFalsy(); - }); - test('should compare filters array to non array', () => { const f1 = buildQueryFilter( { _type: { match: { query: 'apache', type: 'phrase' } } },