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/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/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/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..11fb287c9ae2c 100644
--- a/src/plugins/data/public/actions/select_range_action.ts
+++ b/src/plugins/data/public/actions/select_range_action.ts
@@ -41,6 +41,38 @@ async function isCompatible(context: SelectRangeActionContext) {
}
}
+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
@@ -48,37 +80,13 @@ 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',
});
},
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..e346b3a6f0c3e 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,
@@ -47,6 +48,82 @@ async function isCompatible(context: ValueClickActionContext) {
}
}
+// 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();
+ // 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)) || [];
+
+ let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(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
@@ -54,66 +131,13 @@ 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',
});
},
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 efafea44167d4..cb6190e81a778 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/mocks.ts b/src/plugins/data/public/mocks.ts
index e3fc0e97af09b..830a236b433de 100644
--- a/src/plugins/data/public/mocks.ts
+++ b/src/plugins/data/public/mocks.ts
@@ -46,6 +46,8 @@ const createStartContract = (): Start => {
return {
actions: {
createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']),
+ selectRangeActionGetFilters: jest.fn(),
+ valueClickActionGetFilters: jest.fn(),
},
autocomplete: autocompleteMock,
search: searchStartMock,
diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts
index f8add047603ae..b0ea231db8e6e 100644
--- a/src/plugins/data/public/plugin.ts
+++ b/src/plugins/data/public/plugin.ts
@@ -55,7 +55,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,
@@ -157,6 +163,8 @@ export class DataPublicPlugin implements Plugin boolean;
changeTimeFilter: typeof changeTimeFilter;
+ convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString;
mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[];
extractTimeFilter: typeof extractTimeFilter;
};
@@ -1848,58 +1852,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:382:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:412: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:384:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:414: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)
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/data/public/types.ts b/src/plugins/data/public/types.ts
index 45160cbf30179..887f5c4ec706e 100644
--- a/src/plugins/data/public/types.ts
+++ b/src/plugins/data/public/types.ts
@@ -24,7 +24,11 @@ import { ExpressionsSetup } from 'src/plugins/expressions/public';
import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public';
import { AutocompleteSetup, AutocompleteStart } from './autocomplete';
import { FieldFormatsSetup, FieldFormatsStart } from './field_formats';
-import { createFiltersFromEvent } from './actions';
+import {
+ createFiltersFromEvent,
+ valueClickActionGetFilters,
+ selectRangeActionGetFilters,
+} from './actions';
import { ISearchSetup, ISearchStart } from './search';
import { QuerySetup, QueryStart } from './query';
import { IndexPatternSelectProps } from './ui/index_pattern_select';
@@ -50,6 +54,8 @@ export interface DataPublicPluginSetup {
export interface DataPublicPluginStart {
actions: {
createFiltersFromEvent: typeof createFiltersFromEvent;
+ valueClickActionGetFilters: typeof valueClickActionGetFilters;
+ selectRangeActionGetFilters: typeof selectRangeActionGetFilters;
};
autocomplete: AutocompleteStart;
indexPatterns: IndexPatternsContract;
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 2a856af7ae916..39760814f06b7 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 && !!eventCount && (
-
+
{eventCount}
)}
diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts
index e29302fd6cc13..87b234a63aa9a 100644
--- a/src/plugins/embeddable/public/lib/triggers/triggers.ts
+++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts
@@ -24,12 +24,13 @@ export interface EmbeddableContext {
embeddable: IEmbeddable;
}
-export interface EmbeddableVisTriggerContext {
- embeddable?: IEmbeddable;
+export interface EmbeddableVisTriggerContext {
+ embeddable?: T;
timeFieldName?: string;
data: {
e?: MouseEvent;
data: unknown;
+ range?: unknown;
};
}
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/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json
index acbca5c33295c..da03456df0219 100644
--- a/x-pack/plugins/dashboard_enhanced/kibana.json
+++ b/x-pack/plugins/dashboard_enhanced/kibana.json
@@ -3,6 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["uiActions", "embeddable", "dashboard", "drilldowns"],
+ "requiredPlugins": ["data","uiActions", "embeddable", "dashboard", "drilldowns", "share"],
"configPath": ["xpack", "dashboardEnhanced"]
}
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..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 = () => {
@@ -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.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/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..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
@@ -5,22 +5,23 @@
*/
import React from 'react';
-import { EuiFormRow, EuiSelect, EuiSwitch } 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;
onCurrentFiltersToggle?: () => void;
onKeepRangeToggle?: () => void;
+ onSearchChange: (searchString: string) => void;
+ isLoading: boolean;
}
export const DashboardDrilldownConfig: React.FC = ({
@@ -31,24 +32,33 @@ export const DashboardDrilldownConfig: React.FC =
onDashboardSelect,
onCurrentFiltersToggle,
onKeepRangeToggle,
+ onSearchChange,
+ isLoading,
}) => {
- // TODO: use i18n below.
+ const selectedTitle = dashboards.find(item => item.value === activeDashboardId)?.label || '';
+
return (
<>
-
- ({ value: id, text: title }))}
- value={activeDashboardId}
- onChange={e => onDashboardSelect(e.target.value)}
+
+
+ async
+ selectedOptions={
+ activeDashboardId ? [{ label: selectedTitle, value: activeDashboardId }] : []
+ }
+ options={dashboards}
+ onChange={([{ value = '' } = { value: '' }]) => onDashboardSelect(value)}
+ onSearchChange={onSearchChange}
+ isLoading={isLoading}
+ singleSelection={{ asPlainText: true }}
+ fullWidth
+ data-test-subj={'dashboardDrilldownSelectDashboard'}
/>
{!!onCurrentFiltersToggle && (
@@ -58,7 +68,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/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts
index 30b3f3c080f49..68f4440b202a3 100644
--- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts
+++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts
@@ -6,17 +6,22 @@
import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public';
import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
+import { SharePluginStart, SharePluginSetup } from '../../../../src/plugins/share/public';
import { DashboardDrilldownsService } from './services';
+import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { DrilldownsSetup, DrilldownsStart } from '../../drilldowns/public';
export interface SetupDependencies {
uiActions: UiActionsSetup;
drilldowns: DrilldownsSetup;
+ share: SharePluginSetup;
}
export interface StartDependencies {
uiActions: UiActionsStart;
drilldowns: DrilldownsStart;
+ share: SharePluginStart;
+ data: DataPublicPluginStart;
}
// eslint-disable-next-line
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 00e74ea570a11..6debc22670b91 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,
+ core: CoreSetup,
plugins: SetupDependencies,
{ enableDrilldowns }: BootstrapParams
) {
@@ -41,10 +41,18 @@ export class DashboardDrilldownsService {
}
}
- setupDrilldowns(core: CoreSetup<{ drilldowns: DrilldownsStart }>, plugins: SetupDependencies) {
+ setupDrilldowns(core: CoreSetup, plugins: SetupDependencies) {
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 getNavigateToApp = async () =>
+ (await core.getStartServices())[0].application.navigateToApp;
+
+ 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);
@@ -53,7 +61,10 @@ export class DashboardDrilldownsService {
plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown);
const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({
- savedObjects,
+ getSavedObjectsClient,
+ getGetUrlGenerator,
+ getNavigateToApp,
+ getDataPluginActions,
});
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 e463cc38b6fbf..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
@@ -4,52 +4,134 @@
* 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';
-export interface CollectConfigContainerProps extends CollectConfigProps {
- params: Params;
+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,
+});
+
+interface CollectConfigContainerState {
+ dashboards: Array>;
+ searchString?: string;
+ isLoading: boolean;
+ selectedDashboard?: EuiComboBoxOptionOption;
+ delay: boolean;
}
-export const CollectConfigContainer: React.FC = ({
- config,
- onConfig,
- params: { savedObjects },
-}) => {
- const [dashboards] = useState([
- { id: 'dashboard1', title: 'Dashboard 1' },
- { id: 'dashboard2', title: 'Dashboard 2' },
- { id: 'dashboard3', title: 'Dashboard 3' },
- { id: 'dashboard4', title: 'Dashboard 4' },
- ]);
-
- useEffect(() => {
- // TODO: Load dashboards...
- }, [savedObjects]);
-
- return (
- {
- onConfig({ ...config, dashboardId });
- }}
- onCurrentFiltersToggle={() =>
- onConfig({
- ...config,
- useCurrentFilters: !config.useCurrentFilters,
- })
+export class CollectConfigContainer extends React.Component<
+ CollectConfigProps,
+ CollectConfigContainerState
+> {
+ private isMounted = true;
+ state = {
+ dashboards: [],
+ isLoading: false,
+ searchString: undefined,
+ selectedDashboard: undefined,
+ 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 => {
+ if (config.dashboardId) {
+ savedObjectsClient
+ .get<{ title: string }>('dashboard', config.dashboardId)
+ .then(dashboard => {
+ if (!this.isMounted) return;
+ this.setState({ selectedDashboard: dashboardSavedObjectToMenuItem(dashboard) });
+ });
}
- onKeepRangeToggle={() =>
- onConfig({
- ...config,
- useCurrentDateRange: !config.useCurrentDateRange,
+ });
+ }
+
+ private readonly debouncedLoadDashboards: (searchString?: string) => void;
+ loadDashboards(searchString?: string) {
+ const currentDashboardId = this.props.context.placeContext.embeddable?.parent?.id;
+ this.setState({ searchString, isLoading: true });
+ this.props.deps.getSavedObjectsClient().then(savedObjectsClient => {
+ savedObjectsClient
+ .find<{ title: string }>({
+ type: 'dashboard',
+ search: searchString ? `${searchString}*` : undefined,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ perPage: 100,
})
- }
- />
- );
-};
+ .then(({ savedObjects }) => {
+ 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 });
+ });
+ });
+ }
+
+ render() {
+ const { config, onConfig } = this.props;
+ const { dashboards, selectedDashboard, isLoading } = this.state;
+
+ return (
+ {
+ onConfig({ ...config, dashboardId });
+ }}
+ onSearchChange={this.debouncedLoadDashboards}
+ onCurrentFiltersToggle={() =>
+ onConfig({
+ ...config,
+ useCurrentFilters: !config.useCurrentFilters,
+ })
+ }
+ onKeepRangeToggle={() =>
+ onConfig({
+ ...config,
+ useCurrentDateRange: !config.useCurrentDateRange,
+ })
+ }
+ />
+ );
+ }
+}
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..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
@@ -4,17 +4,310 @@
* 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 {
+ 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';
+import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public';
+
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 savedObjectsClient = savedObjectsServiceMock.createStartContract().client;
+
+ 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(savedObjectsClient),
+ });
+ 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',
+ },
+ },
+ };
+}
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 9d2a378f08acd..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
@@ -7,18 +7,26 @@
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 { PlaceContext, ActionContext, Config, CollectConfigProps } from './types';
+
import { CollectConfigContainer } from './collect_config';
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 {
- savedObjects: () => Promise;
+ getSavedObjectsClient: () => Promise;
+ getNavigateToApp: () => Promise;
+ getGetUrlGenerator: () => Promise;
+ getDataPluginActions: () => Promise;
}
export class DashboardToDashboardDrilldown
- implements Drilldown {
+ implements Drilldown> {
constructor(protected readonly params: Params) {}
public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN;
@@ -30,15 +38,15 @@ export class DashboardToDashboardDrilldown
public readonly euiIcon = 'dashboardApp';
private readonly ReactCollectConfig: React.FC = props => (
-
+
);
public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
public readonly createConfig = () => ({
- dashboardId: '123',
- useCurrentFilters: true,
- useCurrentDateRange: true,
+ dashboardId: '',
+ useCurrentFilters: false,
+ useCurrentDateRange: false,
});
public readonly isConfigValid = (config: Config): config is Config => {
@@ -46,7 +54,68 @@ export class DashboardToDashboardDrilldown
return true;
};
- public readonly execute = () => {
- alert('Go to another dashboard!');
+ public readonly execute = async (
+ config: Config,
+ context: ActionContext
+ ) => {
+ const getUrlGenerator = await this.params.getGetUrlGenerator();
+ const navigateToApp = await this.params.getNavigateToApp();
+
+ const {
+ selectRangeActionGetFilters,
+ valueClickActionGetFilters,
+ } = await this.params.getDataPluginActions();
+ 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.useCurrentFilters
+ ? 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.useCurrentDateRange ? currentTimeRange : undefined;
+
+ if (context.data.range) {
+ // look up by range
+ const { restOfFilters, timeRangeFilter } =
+ (await selectRangeActionGetFilters({
+ timeFieldName: context.timeFieldName!,
+ data: context.data,
+ })) || {};
+ filters.push(...(restOfFilters || []));
+ if (timeRangeFilter) {
+ timeRange = esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter);
+ }
+ } else {
+ const { restOfFilters, timeRangeFilter } = await valueClickActionGetFilters({
+ timeFieldName: context.timeFieldName!,
+ data: context.data,
+ });
+ filters.push(...(restOfFilters || []));
+ if (timeRangeFilter) {
+ timeRange = esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter);
+ }
+ }
+
+ const dashboardPath = await getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({
+ dashboardId: config.dashboardId,
+ query,
+ timeRange,
+ filters,
+ });
+
+ const dashboardHash = dashboardPath.split('#')[1];
+
+ 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 398a259491e3e..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
@@ -4,14 +4,18 @@
* 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 { DrilldownFactoryContext } from '../../../../../drilldowns/public';
import {
EmbeddableVisTriggerContext,
EmbeddableContext,
+ IEmbeddable,
} from '../../../../../../../src/plugins/embeddable/public';
import { UiActionsCollectConfigProps } from '../../../../../../../src/plugins/ui_actions/public';
export type PlaceContext = EmbeddableContext;
-export type ActionContext = EmbeddableVisTriggerContext;
+export type ActionContext = EmbeddableVisTriggerContext;
export interface Config {
dashboardId?: string;
@@ -19,4 +23,13 @@ export interface Config {
useCurrentDateRange: boolean;
}
-export type CollectConfigProps = UiActionsCollectConfigProps;
+export interface CollectConfigProps extends UiActionsCollectConfigProps {
+ deps: {
+ getSavedObjectsClient: () => Promise;
+ getNavigateToApp: () => Promise;
+ getGetUrlGenerator: () => Promise;
+ };
+ context: DrilldownFactoryContext<{
+ embeddable: IEmbeddable;
+ }>;
+}
diff --git a/x-pack/plugins/dashboard_enhanced/server/config.ts b/x-pack/plugins/dashboard_enhanced/server/config.ts
index b75c95d5f8832..1c25a05151e58 100644
--- a/x-pack/plugins/dashboard_enhanced/server/config.ts
+++ b/x-pack/plugins/dashboard_enhanced/server/config.ts
@@ -9,7 +9,7 @@ import { PluginConfigDescriptor } from '../../../../src/core/server';
export const configSchema = schema.object({
drilldowns: schema.object({
- enabled: schema.boolean({ defaultValue: false }),
+ enabled: schema.boolean({ defaultValue: true }),
}),
});
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,
},
diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx
index 8c6739a8ad6c8..48e17dadc810f 100644
--- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx
+++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx
@@ -23,7 +23,7 @@ export interface DrilldownHelloBarProps {
onHideClick?: () => 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 bdafaaf07873c..93b3710bf6cc6 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
@@ -46,7 +46,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/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';
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 0000000000000..febf312799eee
Binary files /dev/null and b/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz differ
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/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.test.tsx b/x-pack/test/functional/services/dashboard/index.ts
similarity index 58%
rename from x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.test.tsx
rename to x-pack/test/functional/services/dashboard/index.ts
index 95101605ce468..dee525fa0a388 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.test.tsx
+++ b/x-pack/test/functional/services/dashboard/index.ts
@@ -4,6 +4,5 @@
* 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');
+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,
};