diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md
new file mode 100644
index 0000000000000..f1916e89c2c98
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilter](./kibana-plugin-plugins-data-public.isfilter.md)
+
+## isFilter variable
+
+Signature:
+
+```typescript
+isFilter: (x: unknown) => x is Filter
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md
new file mode 100644
index 0000000000000..558da72cc26bb
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilters](./kibana-plugin-plugins-data-public.isfilters.md)
+
+## isFilters variable
+
+Signature:
+
+```typescript
+isFilters: (x: unknown) => x is Filter[]
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md
new file mode 100644
index 0000000000000..0884566333aa8
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isQuery](./kibana-plugin-plugins-data-public.isquery.md)
+
+## isQuery variable
+
+Signature:
+
+```typescript
+isQuery: (x: unknown) => x is Query
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md
new file mode 100644
index 0000000000000..e9420493c82fb
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md)
+
+## isTimeRange variable
+
+Signature:
+
+```typescript
+isTimeRange: (x: unknown) => x is TimeRange
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index f62479f02926e..feeb686a1f5ed 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -110,6 +110,10 @@
| [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} |
| [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | |
| [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | |
+| [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) | |
+| [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) | |
+| [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | |
+| [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | |
| [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | |
| [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | |
| [search](./kibana-plugin-plugins-data-public.search.md) | |
diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts
index ff6dff9d8b749..e3099ae6a4026 100644
--- a/src/plugins/data/common/es_query/filters/meta_filter.ts
+++ b/src/plugins/data/common/es_query/filters/meta_filter.ts
@@ -107,3 +107,13 @@ export const pinFilter = (filter: Filter) =>
export const unpinFilter = (filter: Filter) =>
!isFilterPinned(filter) ? filter : toggleFilterPinned(filter);
+
+export const isFilter = (x: unknown): x is Filter =>
+ !!x &&
+ typeof x === 'object' &&
+ !!(x as Filter).meta &&
+ typeof (x as Filter).meta === 'object' &&
+ typeof (x as Filter).meta.disabled === 'boolean';
+
+export const isFilters = (x: unknown): x is Filter[] =>
+ Array.isArray(x) && !x.find((y) => !isFilter(y));
diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts
index adbd93d518fc7..b40e02b709d30 100644
--- a/src/plugins/data/common/index.ts
+++ b/src/plugins/data/common/index.ts
@@ -20,11 +20,12 @@
export * from './constants';
export * from './es_query';
export * from './field_formats';
+export * from './field_mapping';
export * from './index_patterns';
export * from './kbn_field_types';
export * from './query';
export * from './search';
export * from './search/aggs';
+export * from './timefilter';
export * from './types';
export * from './utils';
-export * from './field_mapping';
diff --git a/src/plugins/data/common/query/index.ts b/src/plugins/data/common/query/index.ts
index 421cc4f63e4ef..4e90f6f8bb83e 100644
--- a/src/plugins/data/common/query/index.ts
+++ b/src/plugins/data/common/query/index.ts
@@ -19,3 +19,4 @@
export * from './filter_manager';
export * from './types';
+export * from './is_query';
diff --git a/src/plugins/data/common/query/is_query.ts b/src/plugins/data/common/query/is_query.ts
new file mode 100644
index 0000000000000..08a99a39b1ac1
--- /dev/null
+++ b/src/plugins/data/common/query/is_query.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Query } from './types';
+
+export const isQuery = (x: unknown): x is Query =>
+ !!x &&
+ typeof x === 'object' &&
+ typeof (x as Query).language === 'string' &&
+ (typeof (x as Query).query === 'string' ||
+ (typeof (x as Query).query === 'object' && !!(x as Query).query));
diff --git a/src/plugins/data/common/timefilter/index.ts b/src/plugins/data/common/timefilter/index.ts
new file mode 100644
index 0000000000000..e0c509e119fda
--- /dev/null
+++ b/src/plugins/data/common/timefilter/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { isTimeRange } from './is_time_range';
diff --git a/src/plugins/data/common/timefilter/is_time_range.ts b/src/plugins/data/common/timefilter/is_time_range.ts
new file mode 100644
index 0000000000000..f206cd04dde31
--- /dev/null
+++ b/src/plugins/data/common/timefilter/is_time_range.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { TimeRange } from './types';
+
+export const isTimeRange = (x: unknown): x is TimeRange =>
+ !!x &&
+ typeof x === 'object' &&
+ typeof (x as TimeRange).from === 'string' &&
+ typeof (x as TimeRange).to === 'string';
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index b0ceb6e642ee0..030d462141b09 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -439,6 +439,8 @@ export {
getKbnTypeNames,
} from '../common';
+export { isTimeRange, isQuery, isFilter, isFilters } from '../common';
+
export * from '../common/field_mapping';
/*
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index aa9fd99e6e5ae..b1f151bb09ae6 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -1301,6 +1301,26 @@ export interface ISearchStrategy {
search: ISearch;
}
+// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const isFilter: (x: unknown) => x is Filter;
+
+// Warning: (ae-missing-release-tag) "isFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const isFilters: (x: unknown) => x is Filter[];
+
+// Warning: (ae-missing-release-tag) "isQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const isQuery: (x: unknown) => x is Query;
+
+// Warning: (ae-missing-release-tag) "isTimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const isTimeRange: (x: unknown) => x is TimeRange;
+
// Warning: (ae-missing-release-tag) "ISyncSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts
index 4154fdfeb3ff4..6ac8f674b6153 100644
--- a/src/plugins/discover/public/index.ts
+++ b/src/plugins/discover/public/index.ts
@@ -27,4 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) {
export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
-export { DISCOVER_APP_URL_GENERATOR } from './url_generator';
+export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator';
diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts
index cca63cd880b60..2c6bbcc3ecce1 100644
--- a/src/plugins/discover/public/kibana_services.ts
+++ b/src/plugins/discover/public/kibana_services.ts
@@ -60,10 +60,23 @@ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter createHashHistory());
+/**
+ * Discover currently uses two `history` instances: one from Kibana Platform and
+ * another from `history` package. Below function is used every time Discover
+ * app is loaded to synchronize both instances.
+ *
+ * This helper is temporary until https://github.com/elastic/kibana/issues/65161 is resolved.
+ */
+export const syncHistoryLocations = () => {
+ const h = getHistory();
+ Object.assign(h.location, createHashHistory().location);
+ return h;
+};
+
export const [getScopedHistory, setScopedHistory] = createGetterSetter(
'scopedHistory'
);
diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts
index ad82e0a479815..0e5c5343b6f96 100644
--- a/src/plugins/discover/public/plugin.ts
+++ b/src/plugins/discover/public/plugin.ts
@@ -55,6 +55,7 @@ import {
setServices,
setScopedHistory,
getScopedHistory,
+ syncHistoryLocations,
getServices,
} from './kibana_services';
import { createSavedSearchesLoader } from './saved_searches';
@@ -245,6 +246,7 @@ export class DiscoverPlugin
throw Error('Discover plugin method initializeInnerAngular is undefined');
}
setScopedHistory(params.history);
+ syncHistoryLocations();
appMounted();
const {
plugins: { data: dataStart },
diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts
index 42d689050d5ad..c7f2e2147e819 100644
--- a/src/plugins/discover/public/url_generator.ts
+++ b/src/plugins/discover/public/url_generator.ts
@@ -98,11 +98,13 @@ export class DiscoverUrlGenerator
const queryState: QueryState = {};
if (query) appState.query = query;
- if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
+ if (filters && filters.length)
+ appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
if (indexPatternId) appState.index = indexPatternId;
if (timeRange) queryState.time = timeRange;
- if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
+ if (filters && filters.length)
+ queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
if (refreshInterval) queryState.refreshInterval = refreshInterval;
let url = `${this.params.appBasePath}#/${savedSearchPath}`;
diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json
index 535527b4d09db..74119ca158685 100644
--- a/src/plugins/embeddable/kibana.json
+++ b/src/plugins/embeddable/kibana.json
@@ -4,6 +4,7 @@
"server": false,
"ui": true,
"requiredPlugins": [
+ "data",
"inspector",
"uiActions"
]
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index 1d1dc79121937..9f0ccbe2b00ef 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -28,6 +28,7 @@ export {
ACTION_EDIT_PANEL,
Adapters,
AddPanelAction,
+ ChartActionContext,
Container,
ContainerInput,
ContainerOutput,
diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts
index 2b447c89e2850..5bb96a708b7ac 100644
--- a/src/plugins/embeddable/public/lib/triggers/triggers.ts
+++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts
@@ -39,10 +39,6 @@ export interface ValueClickTriggerContext {
};
}
-export const isValueClickTriggerContext = (
- context: ValueClickTriggerContext | RangeSelectTriggerContext
-): context is ValueClickTriggerContext => context.data && 'data' in context.data;
-
export interface RangeSelectTriggerContext {
embeddable?: T;
data: {
@@ -53,8 +49,16 @@ export interface RangeSelectTriggerContext
};
}
+export type ChartActionContext =
+ | ValueClickTriggerContext
+ | RangeSelectTriggerContext;
+
+export const isValueClickTriggerContext = (
+ context: ChartActionContext
+): context is ValueClickTriggerContext => context.data && 'data' in context.data;
+
export const isRangeSelectTriggerContext = (
- context: ValueClickTriggerContext | RangeSelectTriggerContext
+ context: ChartActionContext
): context is RangeSelectTriggerContext => context.data && 'range' in context.data;
export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER';
diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx
index 49910525c7ab1..c98416cb3e8c7 100644
--- a/src/plugins/embeddable/public/mocks.tsx
+++ b/src/plugins/embeddable/public/mocks.tsx
@@ -31,6 +31,7 @@ import { coreMock } from '../../../core/public/mocks';
import { UiActionsService } from './lib/ui_actions';
import { CoreStart } from '../../../core/public';
import { Start as InspectorStart } from '../../inspector/public';
+import { dataPluginMock } from '../../data/public/mocks';
// eslint-disable-next-line
import { inspectorPluginMock } from '../../inspector/public/mocks';
@@ -100,6 +101,8 @@ const createStartContract = (): Start => {
EmbeddablePanel: jest.fn(),
getEmbeddablePanel: jest.fn(),
getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
+ filtersAndTimeRangeFromContext: jest.fn(),
+ filtersFromContext: jest.fn(),
};
return startContract;
};
@@ -108,11 +111,13 @@ const createInstance = (setupPlugins: Partial = {})
const plugin = new EmbeddablePublicPlugin({} as any);
const setup = plugin.setup(coreMock.createSetup(), {
uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(),
+ data: dataPluginMock.createSetupContract(),
});
const doStart = (startPlugins: Partial = {}) =>
plugin.start(coreMock.createStart(), {
uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(),
inspector: inspectorPluginMock.createStartContract(),
+ data: dataPluginMock.createStartContract(),
});
return {
plugin,
diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx
index c4e0ca44a4e7e..03bb4a4779267 100644
--- a/src/plugins/embeddable/public/plugin.tsx
+++ b/src/plugins/embeddable/public/plugin.tsx
@@ -17,6 +17,13 @@
* under the License.
*/
import React from 'react';
+import {
+ DataPublicPluginSetup,
+ DataPublicPluginStart,
+ Filter,
+ TimeRange,
+ esFilters,
+} from '../../data/public';
import { getSavedObjectFinder } from '../../saved_objects/public';
import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public';
import { Start as InspectorStart } from '../../inspector/public';
@@ -36,15 +43,20 @@ import {
defaultEmbeddableFactoryProvider,
IEmbeddable,
EmbeddablePanel,
+ ChartActionContext,
+ isRangeSelectTriggerContext,
+ isValueClickTriggerContext,
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
import { EmbeddableStateTransfer } from './lib/state_transfer';
export interface EmbeddableSetupDependencies {
+ data: DataPublicPluginSetup;
uiActions: UiActionsSetup;
}
export interface EmbeddableStartDependencies {
+ data: DataPublicPluginStart;
uiActions: UiActionsStart;
inspector: InspectorStart;
}
@@ -70,6 +82,19 @@ export interface EmbeddableStart {
embeddableFactoryId: string
) => EmbeddableFactory | undefined;
getEmbeddableFactories: () => IterableIterator;
+
+ /**
+ * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries.
+ */
+ filtersFromContext: (context: ChartActionContext) => Promise;
+
+ /**
+ * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object.
+ */
+ filtersAndTimeRangeFromContext: (
+ context: ChartActionContext
+ ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>;
+
EmbeddablePanel: EmbeddablePanelHOC;
getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC;
getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer;
@@ -107,7 +132,7 @@ export class EmbeddablePublicPlugin implements Plugin {
this.embeddableFactories.set(
@@ -121,6 +146,41 @@ export class EmbeddablePublicPlugin implements Plugin {
+ try {
+ if (isRangeSelectTriggerContext(context))
+ return await data.actions.createFiltersFromRangeSelectAction(context.data);
+ if (isValueClickTriggerContext(context))
+ return await data.actions.createFiltersFromValueClickAction(context.data);
+ // eslint-disable-next-line no-console
+ console.warn("Can't extract filters from action.", context);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.warn('Error extracting filters from action. Returning empty filter list.', error);
+ }
+ return [];
+ };
+
+ const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async (
+ context
+ ) => {
+ const filters = await filtersFromContext(context);
+
+ if (!context.data.timeFieldName) return { filters };
+
+ const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
+ context.data.timeFieldName,
+ filters
+ );
+
+ return {
+ filters: restOfFilters,
+ timeRange: timeRangeFilter
+ ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter)
+ : undefined,
+ };
+ };
+
const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({
embeddable,
hideHeader,
@@ -146,6 +206,8 @@ export class EmbeddablePublicPlugin implements Plugin {
return history
? new EmbeddableStateTransfer(core.application.navigateToApp, history)
diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts
index e13a906e30338..bb12e3d7b9011 100644
--- a/src/plugins/embeddable/public/tests/test_plugin.ts
+++ b/src/plugins/embeddable/public/tests/test_plugin.ts
@@ -23,6 +23,7 @@ import { UiActionsStart } from '../../../ui_actions/public';
import { uiActionsPluginMock } from '../../../ui_actions/public/mocks';
// eslint-disable-next-line
import { inspectorPluginMock } from '../../../inspector/public/mocks';
+import { dataPluginMock } from '../../../data/public/mocks';
import { coreMock } from '../../../../core/public/mocks';
import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin';
@@ -42,7 +43,10 @@ export const testPlugin = (
const uiActions = uiActionsPluginMock.createPlugin(coreSetup, coreStart);
const initializerContext = {} as any;
const plugin = new EmbeddablePublicPlugin(initializerContext);
- const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup });
+ const setup = plugin.setup(coreSetup, {
+ data: dataPluginMock.createSetupContract(),
+ uiActions: uiActions.setup,
+ });
return {
plugin,
@@ -51,8 +55,9 @@ export const testPlugin = (
setup,
doStart: (anotherCoreStart: CoreStart = coreStart) => {
const start = plugin.start(anotherCoreStart, {
- uiActions: uiActionsPluginMock.createStartContract(),
+ data: dataPluginMock.createStartContract(),
inspector: inspectorPluginMock.createStartContract(),
+ uiActions: uiActionsPluginMock.createStartContract(),
});
return start;
},
diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts
index c9a5dcfba32b1..0f5d6ea74a6b6 100644
--- a/test/functional/services/dashboard/panel_actions.ts
+++ b/test/functional/services/dashboard/panel_actions.ts
@@ -213,5 +213,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft
await testSubjects.click('saveNewTitleButton');
await this.toggleContextMenu(panel);
}
+
+ async getActionWebElementByText(text: string): Promise {
+ log.debug(`getActionWebElement: "${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 item;
+ }
+ }
+
+ throw new Error(`No action matching text "${text}"`);
+ }
})();
}
diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts
index 66f32d246b31f..a25695a5bfcb7 100644
--- a/test/functional/services/visualizations/pie_chart.ts
+++ b/test/functional/services/visualizations/pie_chart.ts
@@ -28,10 +28,13 @@ export function PieChartProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const find = getService('find');
const defaultFindTimeout = config.get('timeouts.find');
+ const panelActions = getService('dashboardPanelActions');
return new (class PieChart {
- async filterOnPieSlice(name?: string) {
- log.debug(`PieChart.filterOnPieSlice(${name})`);
+ private readonly filterActionText = 'Apply filter to current view';
+
+ async clickOnPieSlice(name?: string) {
+ log.debug(`PieChart.clickOnPieSlice(${name})`);
if (name) {
await testSubjects.click(`pieSlice-${name.split(' ').join('-')}`);
} else {
@@ -44,6 +47,16 @@ export function PieChartProvider({ getService }: FtrProviderContext) {
}
}
+ async filterOnPieSlice(name?: string) {
+ log.debug(`PieChart.filterOnPieSlice(${name})`);
+ await this.clickOnPieSlice(name);
+ const hasUiActionsPopup = await testSubjects.exists('multipleActionsContextMenu');
+ if (hasUiActionsPopup) {
+ const actionElement = await panelActions.getActionWebElementByText(this.filterActionText);
+ await actionElement.click();
+ }
+ }
+
async filterByLegendItem(label: string) {
log.debug(`PieChart.filterByLegendItem(${label})`);
await testSubjects.click(`legend-${label}`);
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts
new file mode 100644
index 0000000000000..620cabe652778
--- /dev/null
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { DiscoverStart } from '../../../../../../src/plugins/discover/public';
+import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public';
+import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
+import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
+import { CoreStart } from '../../../../../../src/core/public';
+import { KibanaURL } from './kibana_url';
+import * as shared from './shared';
+
+export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
+
+export interface PluginDeps {
+ discover: Pick;
+ embeddable: Pick;
+}
+
+export interface CoreDeps {
+ application: Pick;
+}
+
+export interface Params {
+ start: StartServicesGetter;
+}
+
+export abstract class AbstractExploreDataAction {
+ public readonly getIconType = (context: Context): string => 'discoverApp';
+
+ public readonly getDisplayName = (context: Context): string =>
+ i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', {
+ defaultMessage: 'Explore underlying data',
+ });
+
+ constructor(protected readonly params: Params) {}
+
+ protected abstract async getUrl(context: Context): Promise;
+
+ public async isCompatible({ embeddable }: Context): Promise {
+ if (!embeddable) return false;
+ if (!this.params.start().plugins.discover.urlGenerator) return false;
+ if (!shared.isVisualizeEmbeddable(embeddable)) return false;
+ if (!shared.getIndexPattern(embeddable)) return false;
+ if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false;
+ return true;
+ }
+
+ public async execute(context: Context): Promise {
+ if (!shared.isVisualizeEmbeddable(context.embeddable)) return;
+
+ const { core } = this.params.start();
+ const { appName, appPath } = await this.getUrl(context);
+
+ await core.application.navigateToApp(appName, {
+ path: appPath,
+ });
+ }
+
+ public async getHref(context: Context): Promise {
+ const { embeddable } = context;
+
+ if (!shared.isVisualizeEmbeddable(embeddable)) {
+ throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`);
+ }
+
+ const { path } = await this.getUrl(context);
+
+ return path;
+ }
+}
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts
new file mode 100644
index 0000000000000..a273f0d50e45e
--- /dev/null
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts
@@ -0,0 +1,274 @@
+/*
+ * 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 { ExploreDataChartAction } from './explore_data_chart_action';
+import { Params, PluginDeps } from './abstract_explore_data_action';
+import { coreMock } from '../../../../../../src/core/public/mocks';
+import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
+import {
+ EmbeddableStart,
+ RangeSelectTriggerContext,
+ ValueClickTriggerContext,
+ ChartActionContext,
+} from '../../../../../../src/plugins/embeddable/public';
+import { i18n } from '@kbn/i18n';
+import {
+ VisualizeEmbeddableContract,
+ VISUALIZE_EMBEDDABLE_TYPE,
+} from '../../../../../../src/plugins/visualizations/public';
+import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
+import { Filter, TimeRange } from '../../../../../../src/plugins/data/public';
+
+const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance;
+
+jest.mock('@kbn/i18n', () => ({
+ i18n: {
+ translate: jest.fn((key, options) => options.defaultMessage),
+ },
+}));
+
+afterEach(() => {
+ i18nTranslateSpy.mockClear();
+});
+
+const setup = ({ useRangeEvent = false }: { useRangeEvent?: boolean } = {}) => {
+ type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
+
+ const core = coreMock.createStart();
+
+ const urlGenerator: UrlGenerator = ({
+ createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
+ } as unknown) as UrlGenerator;
+
+ const filtersAndTimeRangeFromContext = jest.fn((async () => ({
+ filters: [],
+ })) as EmbeddableStart['filtersAndTimeRangeFromContext']);
+
+ const plugins: PluginDeps = {
+ discover: {
+ urlGenerator,
+ },
+ embeddable: {
+ filtersAndTimeRangeFromContext,
+ },
+ };
+
+ const params: Params = {
+ start: () => ({
+ plugins,
+ self: {},
+ core,
+ }),
+ };
+ const action = new ExploreDataChartAction(params);
+
+ const input = {
+ viewMode: ViewMode.VIEW,
+ };
+
+ const output = {
+ indexPatterns: [
+ {
+ id: 'index-ptr-foo',
+ },
+ ],
+ };
+
+ const embeddable: VisualizeEmbeddableContract = ({
+ type: VISUALIZE_EMBEDDABLE_TYPE,
+ getInput: () => input,
+ getOutput: () => output,
+ } as unknown) as VisualizeEmbeddableContract;
+
+ const data: ChartActionContext['data'] = {
+ ...(useRangeEvent
+ ? ({ range: {} } as RangeSelectTriggerContext['data'])
+ : ({ data: [] } as ValueClickTriggerContext['data'])),
+ timeFieldName: 'order_date',
+ };
+
+ const context = {
+ embeddable,
+ data,
+ } as ChartActionContext;
+
+ return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context };
+};
+
+describe('"Explore underlying data" panel action', () => {
+ test('action has Discover icon', () => {
+ const { action, context } = setup();
+ expect(action.getIconType(context)).toBe('discoverApp');
+ });
+
+ test('title is "Explore underlying data"', () => {
+ const { action, context } = setup();
+ expect(action.getDisplayName(context)).toBe('Explore underlying data');
+ });
+
+ test('translates title', () => {
+ expect(i18nTranslateSpy).toHaveBeenCalledTimes(0);
+
+ const { action, context } = setup();
+ action.getDisplayName(context);
+
+ expect(i18nTranslateSpy).toHaveBeenCalledTimes(1);
+ expect(i18nTranslateSpy.mock.calls[0][0]).toBe(
+ 'xpack.discover.FlyoutCreateDrilldownAction.displayName'
+ );
+ });
+
+ describe('isCompatible()', () => {
+ test('returns true when all conditions are met', async () => {
+ const { action, context } = setup();
+
+ const isCompatible = await action.isCompatible(context);
+
+ expect(isCompatible).toBe(true);
+ });
+
+ test('returns false when URL generator is not present', async () => {
+ const { action, plugins, context } = setup();
+ (plugins.discover as any).urlGenerator = undefined;
+
+ const isCompatible = await action.isCompatible(context);
+
+ expect(isCompatible).toBe(false);
+ });
+
+ test('returns false if embeddable is not Visualize embeddable', async () => {
+ const { action, embeddable, context } = setup();
+ (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE';
+
+ const isCompatible = await action.isCompatible(context);
+
+ expect(isCompatible).toBe(false);
+ });
+
+ test('returns false if embeddable does not have index patterns', async () => {
+ const { action, output, context } = setup();
+ delete output.indexPatterns;
+
+ const isCompatible = await action.isCompatible(context);
+
+ expect(isCompatible).toBe(false);
+ });
+
+ test('returns false if embeddable index patterns are empty', async () => {
+ const { action, output, context } = setup();
+ output.indexPatterns = [];
+
+ const isCompatible = await action.isCompatible(context);
+
+ expect(isCompatible).toBe(false);
+ });
+
+ test('returns false if dashboard is in edit mode', async () => {
+ const { action, input, context } = setup();
+ input.viewMode = ViewMode.EDIT;
+
+ const isCompatible = await action.isCompatible(context);
+
+ expect(isCompatible).toBe(false);
+ });
+ });
+
+ describe('getHref()', () => {
+ test('returns URL path generated by URL generator', async () => {
+ const { action, context } = setup();
+
+ const href = await action.getHref(context);
+
+ expect(href).toBe('/xyz/app/discover/foo#bar');
+ });
+
+ test('calls URL generator with right arguments', async () => {
+ const { action, urlGenerator, context } = setup();
+
+ expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0);
+
+ await action.getHref(context);
+
+ expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1);
+ expect(urlGenerator.createUrl).toHaveBeenCalledWith({
+ filters: [],
+ indexPatternId: 'index-ptr-foo',
+ timeRange: undefined,
+ });
+ });
+
+ test('applies chart event filters', async () => {
+ const { action, context, urlGenerator, plugins } = setup();
+
+ ((plugins.embeddable
+ .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => {
+ const filters: Filter[] = [
+ {
+ meta: {
+ alias: 'alias',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ];
+ const timeRange: TimeRange = {
+ from: 'from',
+ to: 'to',
+ };
+ return { filters, timeRange };
+ });
+
+ expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0);
+
+ await action.getHref(context);
+
+ expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1);
+ expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context);
+ expect(urlGenerator.createUrl).toHaveBeenCalledWith({
+ filters: [
+ {
+ meta: {
+ alias: 'alias',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ indexPatternId: 'index-ptr-foo',
+ timeRange: {
+ from: 'from',
+ to: 'to',
+ },
+ });
+ });
+ });
+
+ describe('execute()', () => {
+ test('calls platform SPA navigation method', async () => {
+ const { action, context, core } = setup();
+
+ expect(core.application.navigateToApp).toHaveBeenCalledTimes(0);
+
+ await action.execute(context);
+
+ expect(core.application.navigateToApp).toHaveBeenCalledTimes(1);
+ });
+
+ test('calls platform SPA navigation method with right arguments', async () => {
+ const { action, context, core } = setup();
+
+ await action.execute(context);
+
+ expect(core.application.navigateToApp).toHaveBeenCalledTimes(1);
+ expect(core.application.navigateToApp.mock.calls[0]).toEqual([
+ 'discover',
+ {
+ path: '/foo#bar',
+ },
+ ]);
+ });
+ });
+});
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts
new file mode 100644
index 0000000000000..359f14959c6a6
--- /dev/null
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Action } from '../../../../../../src/plugins/ui_actions/public';
+import {
+ ValueClickTriggerContext,
+ RangeSelectTriggerContext,
+} from '../../../../../../src/plugins/embeddable/public';
+import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public';
+import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public';
+import { KibanaURL } from './kibana_url';
+import * as shared from './shared';
+import { AbstractExploreDataAction } from './abstract_explore_data_action';
+
+export type ExploreDataChartActionContext = ValueClickTriggerContext | RangeSelectTriggerContext;
+
+export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART';
+
+/**
+ * This is "Explore underlying data" action which appears in popup context
+ * menu when user clicks a value in visualization or brushes a time range.
+ */
+export class ExploreDataChartAction extends AbstractExploreDataAction
+ implements Action {
+ public readonly id = ACTION_EXPLORE_DATA_CHART;
+
+ public readonly type = ACTION_EXPLORE_DATA_CHART;
+
+ public readonly order = 200;
+
+ protected readonly getUrl = async (
+ context: ExploreDataChartActionContext
+ ): Promise => {
+ const { plugins } = this.params.start();
+ const { urlGenerator } = plugins.discover;
+
+ if (!urlGenerator) {
+ throw new Error('Discover URL generator not available.');
+ }
+
+ const { embeddable } = context;
+ const { filters, timeRange } = await plugins.embeddable.filtersAndTimeRangeFromContext(context);
+ const state: DiscoverUrlGeneratorState = {
+ filters,
+ timeRange,
+ };
+
+ if (embeddable) {
+ state.indexPatternId = shared.getIndexPattern(embeddable) || undefined;
+
+ const input = embeddable.getInput();
+
+ if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange;
+ if (isQuery(input.query)) state.query = input.query;
+ if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])];
+ }
+
+ const path = await urlGenerator.createUrl(state);
+
+ return new KibanaURL(path);
+ };
+}
diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts
similarity index 88%
rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts
rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts
index a7167d2e2e691..e742b69380973 100644
--- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts
@@ -4,14 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- ExploreDataContextMenuAction,
- ACTION_EXPLORE_DATA,
- Params,
- PluginDeps,
-} from './explore_data_context_menu_action';
+import { ExploreDataContextMenuAction } from './explore_data_context_menu_action';
+import { Params, PluginDeps } from './abstract_explore_data_action';
import { coreMock } from '../../../../../../src/core/public/mocks';
import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
+import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public';
import { i18n } from '@kbn/i18n';
import {
VisualizeEmbeddableContract,
@@ -37,14 +34,20 @@ const setup = () => {
const core = coreMock.createStart();
const urlGenerator: UrlGenerator = ({
- id: ACTION_EXPLORE_DATA,
createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
} as unknown) as UrlGenerator;
+ const filtersAndTimeRangeFromContext = jest.fn((async () => ({
+ filters: [],
+ })) as EmbeddableStart['filtersAndTimeRangeFromContext']);
+
const plugins: PluginDeps = {
discover: {
urlGenerator,
},
+ embeddable: {
+ filtersAndTimeRangeFromContext,
+ },
};
const params: Params = {
@@ -83,19 +86,20 @@ const setup = () => {
describe('"Explore underlying data" panel action', () => {
test('action has Discover icon', () => {
- const { action } = setup();
- expect(action.getIconType()).toBe('discoverApp');
+ const { action, context } = setup();
+ expect(action.getIconType(context)).toBe('discoverApp');
});
test('title is "Explore underlying data"', () => {
- const { action } = setup();
- expect(action.getDisplayName()).toBe('Explore underlying data');
+ const { action, context } = setup();
+ expect(action.getDisplayName(context)).toBe('Explore underlying data');
});
test('translates title', () => {
expect(i18nTranslateSpy).toHaveBeenCalledTimes(0);
- setup().action.getDisplayName();
+ const { action, context } = setup();
+ action.getDisplayName(context);
expect(i18nTranslateSpy).toHaveBeenCalledTimes(1);
expect(i18nTranslateSpy.mock.calls[0][0]).toBe(
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts
new file mode 100644
index 0000000000000..6691089f875d8
--- /dev/null
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Action } from '../../../../../../src/plugins/ui_actions/public';
+import { EmbeddableContext } from '../../../../../../src/plugins/embeddable/public';
+import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public';
+import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public';
+import { KibanaURL } from './kibana_url';
+import * as shared from './shared';
+import { AbstractExploreDataAction } from './abstract_explore_data_action';
+
+export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
+
+/**
+ * This is "Explore underlying data" action which appears in the context
+ * menu of a dashboard panel.
+ */
+export class ExploreDataContextMenuAction extends AbstractExploreDataAction
+ implements Action {
+ public readonly id = ACTION_EXPLORE_DATA;
+
+ public readonly type = ACTION_EXPLORE_DATA;
+
+ public readonly order = 200;
+
+ protected readonly getUrl = async (context: EmbeddableContext): Promise => {
+ const { plugins } = this.params.start();
+ const { urlGenerator } = plugins.discover;
+
+ if (!urlGenerator) {
+ throw new Error('Discover URL generator not available.');
+ }
+
+ const { embeddable } = context;
+ const state: DiscoverUrlGeneratorState = {};
+
+ if (embeddable) {
+ state.indexPatternId = shared.getIndexPattern(embeddable) || undefined;
+
+ const input = embeddable.getInput();
+
+ if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange;
+ if (isQuery(input.query)) state.query = input.query;
+ if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])];
+ }
+
+ const path = await urlGenerator.createUrl(state);
+
+ return new KibanaURL(path);
+ };
+}
diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts
similarity index 86%
rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts
rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts
index 8788621365385..e6d7d4b59149e 100644
--- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts
@@ -5,3 +5,4 @@
*/
export * from './explore_data_context_menu_action';
+export * from './explore_data_chart_action';
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts
new file mode 100644
index 0000000000000..3c25fc2b3c3d1
--- /dev/null
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+// TODO: Replace this logic with KibanaURL once it is available.
+// https://github.com/elastic/kibana/issues/64497
+export class KibanaURL {
+ public readonly path: string;
+ public readonly appName: string;
+ public readonly appPath: string;
+
+ constructor(path: string) {
+ const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/);
+
+ if (!match) {
+ throw new Error('Unexpected Discover URL path.');
+ }
+
+ const [, appName, appPath] = match;
+
+ if (!appName || !appPath) {
+ throw new Error('Could not parse Discover URL path.');
+ }
+
+ this.path = path;
+ this.appName = appName;
+ this.appPath = appPath;
+ }
+}
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts
new file mode 100644
index 0000000000000..fa2168df944b0
--- /dev/null
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
+import {
+ VISUALIZE_EMBEDDABLE_TYPE,
+ VisualizeEmbeddableContract,
+} from '../../../../../../src/plugins/visualizations/public';
+
+export const isOutputWithIndexPatterns = (
+ output: unknown
+): output is { indexPatterns: Array<{ id: string }> } => {
+ if (!output || typeof output !== 'object') return false;
+ return Array.isArray((output as any).indexPatterns);
+};
+
+export const isVisualizeEmbeddable = (
+ embeddable?: IEmbeddable
+): embeddable is VisualizeEmbeddableContract =>
+ embeddable && embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE ? true : false;
+
+/**
+ * @returns Returns empty string if no index pattern ID found.
+ */
+export const getIndexPattern = (embeddable?: IEmbeddable): string => {
+ if (!embeddable) return '';
+ const output = embeddable.getOutput();
+
+ if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) {
+ return output.indexPatterns[0].id;
+ }
+
+ return '';
+};
diff --git a/x-pack/plugins/discover_enhanced/public/actions/index.ts b/x-pack/plugins/discover_enhanced/public/actions/index.ts
index cbb955fa46340..209ae6bee09b5 100644
--- a/x-pack/plugins/discover_enhanced/public/actions/index.ts
+++ b/x-pack/plugins/discover_enhanced/public/actions/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './view_in_discover';
+export * from './explore_data';
diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts
deleted file mode 100644
index d66ca129934a8..0000000000000
--- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts
+++ /dev/null
@@ -1,156 +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.
- */
-
-/* eslint-disable max-classes-per-file */
-
-import { i18n } from '@kbn/i18n';
-import { Action } from '../../../../../../src/plugins/ui_actions/public';
-import { DiscoverStart } from '../../../../../../src/plugins/discover/public';
-import {
- EmbeddableContext,
- IEmbeddable,
- ViewMode,
-} from '../../../../../../src/plugins/embeddable/public';
-import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
-import { CoreStart } from '../../../../../../src/core/public';
-import {
- VisualizeEmbeddableContract,
- VISUALIZE_EMBEDDABLE_TYPE,
-} from '../../../../../../src/plugins/visualizations/public';
-
-// TODO: Replace this logic with KibanaURL once it is available.
-// https://github.com/elastic/kibana/issues/64497
-class KibanaURL {
- public readonly path: string;
- public readonly appName: string;
- public readonly appPath: string;
-
- constructor(path: string) {
- const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/);
-
- if (!match) {
- throw new Error('Unexpected Discover URL path.');
- }
-
- const [, appName, appPath] = match;
-
- if (!appName || !appPath) {
- throw new Error('Could not parse Discover URL path.');
- }
-
- this.path = path;
- this.appName = appName;
- this.appPath = appPath;
- }
-}
-
-export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
-
-const isOutputWithIndexPatterns = (
- output: unknown
-): output is { indexPatterns: Array<{ id: string }> } => {
- if (!output || typeof output !== 'object') return false;
- return Array.isArray((output as any).indexPatterns);
-};
-
-const isVisualizeEmbeddable = (
- embeddable: IEmbeddable
-): embeddable is VisualizeEmbeddableContract => embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE;
-
-export interface PluginDeps {
- discover: Pick;
-}
-
-export interface CoreDeps {
- application: Pick;
-}
-
-export interface Params {
- start: StartServicesGetter;
-}
-
-export class ExploreDataContextMenuAction implements Action {
- public readonly id = ACTION_EXPLORE_DATA;
-
- public readonly type = ACTION_EXPLORE_DATA;
-
- public readonly order = 200;
-
- constructor(private readonly params: Params) {}
-
- public getDisplayName() {
- return i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', {
- defaultMessage: 'Explore underlying data',
- });
- }
-
- public getIconType() {
- return 'discoverApp';
- }
-
- public async isCompatible({ embeddable }: EmbeddableContext) {
- if (!this.params.start().plugins.discover.urlGenerator) return false;
- if (!isVisualizeEmbeddable(embeddable)) return false;
- if (!this.getIndexPattern(embeddable)) return false;
- if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false;
- return true;
- }
-
- public async execute({ embeddable }: EmbeddableContext) {
- if (!isVisualizeEmbeddable(embeddable)) return;
-
- const { core } = this.params.start();
- const { appName, appPath } = await this.getUrl(embeddable);
-
- await core.application.navigateToApp(appName, {
- path: appPath,
- });
- }
-
- public async getHref({ embeddable }: EmbeddableContext): Promise {
- if (!isVisualizeEmbeddable(embeddable)) {
- throw new Error(`Embeddable not supported for "${this.getDisplayName()}" action.`);
- }
-
- const { path } = await this.getUrl(embeddable);
-
- return path;
- }
-
- private async getUrl(embeddable: VisualizeEmbeddableContract): Promise {
- const { plugins } = this.params.start();
- const { urlGenerator } = plugins.discover;
-
- if (!urlGenerator) {
- throw new Error('Discover URL generator not available.');
- }
-
- const { timeRange, query, filters } = embeddable.getInput();
- const indexPatternId = this.getIndexPattern(embeddable);
-
- const path = await urlGenerator.createUrl({
- indexPatternId,
- filters,
- query,
- timeRange,
- });
-
- return new KibanaURL(path);
- }
-
- /**
- * @returns Returns empty string if no index pattern ID found.
- */
- private getIndexPattern(embeddable: VisualizeEmbeddableContract): string {
- const output = embeddable!.getOutput();
-
- if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) {
- return output.indexPatterns[0].id;
- }
-
- return '';
- }
-}
diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts
index f55c5dab3449b..ea3c1222eb369 100644
--- a/x-pack/plugins/discover_enhanced/public/plugin.ts
+++ b/x-pack/plugins/discover_enhanced/public/plugin.ts
@@ -6,7 +6,12 @@
import { CoreSetup, CoreStart, Plugin } from 'kibana/public';
import { PluginInitializerContext } from 'kibana/public';
-import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
+import {
+ UiActionsSetup,
+ UiActionsStart,
+ SELECT_RANGE_TRIGGER,
+ VALUE_CLICK_TRIGGER,
+} from '../../../../src/plugins/ui_actions/public';
import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public';
import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public';
@@ -16,11 +21,18 @@ import {
EmbeddableContext,
CONTEXT_MENU_TRIGGER,
} from '../../../../src/plugins/embeddable/public';
-import { ExploreDataContextMenuAction, ACTION_EXPLORE_DATA } from './actions';
+import {
+ ExploreDataContextMenuAction,
+ ExploreDataChartAction,
+ ACTION_EXPLORE_DATA,
+ ACTION_EXPLORE_DATA_CHART,
+ ExploreDataChartActionContext,
+} from './actions';
declare module '../../../../src/plugins/ui_actions/public' {
export interface ActionContextMapping {
[ACTION_EXPLORE_DATA]: EmbeddableContext;
+ [ACTION_EXPLORE_DATA_CHART]: ExploreDataChartActionContext;
}
}
@@ -48,10 +60,17 @@ export class DiscoverEnhancedPlugin
{ uiActions, share }: DiscoverEnhancedSetupDependencies
) {
const start = createStartServicesGetter(core.getStartServices);
+ const isSharePluginInstalled = !!share;
- if (!!share) {
- const exploreDataAction = new ExploreDataContextMenuAction({ start });
+ if (isSharePluginInstalled) {
+ const params = { start };
+
+ const exploreDataAction = new ExploreDataContextMenuAction(params);
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, exploreDataAction);
+
+ const exploreDataChartAction = new ExploreDataChartAction(params);
+ uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction);
+ uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction);
}
}
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts
index bcdd3d1f82e7d..29ead0db1c634 100644
--- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts
@@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
// trigger drilldown action by clicking on a pie and picking drilldown action by it's name
- await pieChart.filterOnPieSlice('40,000');
+ await pieChart.clickOnPieSlice('40,000');
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
const href = await dashboardDrilldownPanelActions.getActionHrefByText(
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts
new file mode 100644
index 0000000000000..12363f8800c28
--- /dev/null
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 ACTION_ID = 'ACTION_EXPLORE_DATA_CHART';
+const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`;
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const drilldowns = getService('dashboardDrilldownsManage');
+ const { dashboard, discover, common, timePicker } = getPageObjects([
+ 'dashboard',
+ 'discover',
+ 'common',
+ 'timePicker',
+ ]);
+ const testSubjects = getService('testSubjects');
+ const pieChart = getService('pieChart');
+ const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions');
+ const filterBar = getService('filterBar');
+ const browser = getService('browser');
+
+ describe('Explore underlying data - chart action', () => {
+ describe('value click action', () => {
+ it('action exists in chart click popup menu', async () => {
+ await common.navigateToApp('dashboard');
+ await dashboard.preserveCrossAppState();
+ await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
+ await pieChart.clickOnPieSlice('160,000');
+ await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
+ await testSubjects.existOrFail(ACTION_TEST_SUBJ);
+ });
+
+ it('action is a link element', async () => {
+ const actionElement = await testSubjects.find(ACTION_TEST_SUBJ);
+ const tag = await actionElement.getTagName();
+ const href = await actionElement.getAttribute('href');
+
+ expect(tag.toLowerCase()).to.be('a');
+ expect(typeof href).to.be('string');
+ expect(href.length > 5).to.be(true);
+ });
+
+ it('navigates to Discover app on action click carrying over pie slice filter', async () => {
+ await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
+ await discover.waitForDiscoverAppOnScreen();
+ await filterBar.hasFilter('memory', '160,000 to 200,000');
+ const filterCount = await filterBar.getFilterCount();
+
+ expect(filterCount).to.be(1);
+ });
+ });
+
+ describe('brush action', () => {
+ let originalTimeRangeDurationHours: number | undefined;
+
+ it('action exists in chart brush popup menu', async () => {
+ await common.navigateToApp('dashboard');
+ await dashboard.preserveCrossAppState();
+ await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_AREA_CHART_NAME);
+
+ originalTimeRangeDurationHours = await timePicker.getTimeDurationInHours();
+ const areaChart = await testSubjects.find('visualizationLoader');
+ await browser.dragAndDrop(
+ {
+ location: areaChart,
+ offset: {
+ x: -100,
+ y: 0,
+ },
+ },
+ {
+ location: areaChart,
+ offset: {
+ x: 100,
+ y: 0,
+ },
+ }
+ );
+
+ await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
+ await testSubjects.existOrFail(ACTION_TEST_SUBJ);
+ });
+
+ it('navigates to Discover on click carrying over brushed time range', async () => {
+ await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
+ await discover.waitForDiscoverAppOnScreen();
+ const newTimeRangeDurationHours = await timePicker.getTimeDurationInHours();
+
+ expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours as number);
+ });
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts
index 24d6e820ac0eb..fedc83a2f81c7 100644
--- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts
@@ -8,7 +8,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
const ACTION_ID = 'ACTION_EXPLORE_DATA';
-const EXPLORE_RAW_DATA_ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`;
+const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`;
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const drilldowns = getService('dashboardDrilldownsManage');
@@ -24,31 +24,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
describe('Explore underlying data - panel action', function () {
- before(async () => {
- await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' });
+ before(
+ 'change default index pattern to verify action navigates to correct index pattern',
+ async () => {
+ await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' });
+ }
+ );
+
+ before('start on Dashboard landing page', async () => {
await common.navigateToApp('dashboard');
await dashboard.preserveCrossAppState();
});
- after(async () => {
+ after('set back default index pattern', async () => {
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
});
+ after('clean-up custom time range on panel', async () => {
+ await common.navigateToApp('dashboard');
+ await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
+ await panelActions.openContextMenu();
+ await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
+ await panelActionsTimeRange.clickRemovePerPanelTimeRangeButton();
+ await dashboard.saveDashboard('Dashboard with Pie Chart');
+ });
+
it('action exists in panel context menu', async () => {
await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
await panelActions.openContextMenu();
- await testSubjects.existOrFail(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
+ await testSubjects.existOrFail(ACTION_TEST_SUBJ);
});
it('is a link element', async () => {
- const actionElement = await testSubjects.find(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
+ const actionElement = await testSubjects.find(ACTION_TEST_SUBJ);
const tag = await actionElement.getTagName();
expect(tag.toLowerCase()).to.be('a');
});
it('navigates to Discover app to index pattern of the panel on action click', async () => {
- await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
+ await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
await discover.waitForDiscoverAppOnScreen();
const el = await testSubjects.find('indexPattern-switch-link');
@@ -71,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboard.saveDashboard('Dashboard with Pie Chart');
await panelActions.openContextMenu();
- await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
+ await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
await discover.waitForDiscoverAppOnScreen();
const text = await timePicker.getShowDatesButtonText();
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
index 19d85ad0e448f..4cdb33c06947f 100644
--- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
@@ -24,5 +24,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
loadTestFile(require.resolve('./dashboard_drilldowns'));
loadTestFile(require.resolve('./explore_data_panel_action'));
+ loadTestFile(require.resolve('./explore_data_chart_action'));
});
}
diff --git a/x-pack/test/functional/services/dashboard/panel_time_range.ts b/x-pack/test/functional/services/dashboard/panel_time_range.ts
index 6a91a6ff0584b..f71e8284c30d9 100644
--- a/x-pack/test/functional/services/dashboard/panel_time_range.ts
+++ b/x-pack/test/functional/services/dashboard/panel_time_range.ts
@@ -52,5 +52,11 @@ export function DashboardPanelTimeRangeProvider({ getService }: FtrProviderConte
const button = await this.findModalTestSubject('addPerPanelTimeRangeButton');
await button.click();
}
+
+ public async clickRemovePerPanelTimeRangeButton() {
+ log.debug('clickRemovePerPanelTimeRangeButton');
+ const button = await this.findModalTestSubject('removePerPanelTimeRangeButton');
+ await button.click();
+ }
})();
}