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, };