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 7fd65e5db35f36..37142cf1794c32 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -49,6 +49,7 @@ esFilters: { generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; + convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; } diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index 4d15e7e899fa89..ff4e50ba8c3275 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -39,7 +39,7 @@ export interface ClonePanelActionContext { export class ClonePanelAction implements ActionByType { public readonly type = ACTION_CLONE_PANEL; public readonly id = ACTION_CLONE_PANEL; - public order = 11; + public order = 45; constructor(private core: CoreStart) {} diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index bd20c6f632a3a5..ebaac6b745bec1 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/index.ts b/src/plugins/data/public/index.ts index e1e2576b2a0e73..cd47719746458c 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/public.api.md b/src/plugins/data/public/public.api.md index 59f56d3983b35e..1e94ec70517882 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -392,6 +392,7 @@ export const esFilters: { generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; + convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; }; @@ -1786,54 +1787,55 @@ 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:381:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts: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:383:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402: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 "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index a6260e782c12ff..3b7de45799a004 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 8da83580ef5d6b..cbbf2f27543129 100644 --- a/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts +++ b/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts @@ -20,7 +20,7 @@ import moment from 'moment'; import { keys } from 'lodash'; import { TimefilterContract } from '../../timefilter'; -import { RangeFilter } from '../../../../common'; +import { RangeFilter, TimeRange } from '../../../../common'; export function convertRangeFilterToTimeRange(filter: RangeFilter) { const key = keys(filter.range)[0]; @@ -32,6 +32,14 @@ export function convertRangeFilterToTimeRange(filter: RangeFilter) { }; } +export function convertRangeFilterToTimeRangeString(filter: RangeFilter): TimeRange { + const { from, to } = convertRangeFilterToTimeRange(filter); + return { + from: from?.toISOString(), + to: to?.toISOString(), + }; +} + export function changeTimeFilter(timeFilter: TimefilterContract, filter: RangeFilter) { timeFilter.setTime(convertRangeFilterToTimeRange(filter)); } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 00761fbb23c780..e61ad2a6eefedd 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -67,6 +67,8 @@ export { withEmbeddableSubscription, SavedObjectEmbeddableInput, isSavedObjectEmbeddableInput, + isRangeSelectTriggerContext, + isValueClickTriggerContext, } from './lib'; export function plugin(initializerContext: PluginInitializerContext) { 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 144913ecbac6f7..35a10ed848e838 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 @@ -64,6 +64,7 @@ function renderNotifications( ) { return notifications.map(notification => ( notification.execute({ embeddable })} diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index fcd9ca6d2186a5..c097e3e8c13be8 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -17,16 +17,16 @@ * under the License. */ -import { Trigger } from '../../../../ui_actions/public'; import { KibanaDatatable } from '../../../../expressions'; +import { Trigger } from '../../../../ui_actions/public'; import { IEmbeddable } from '..'; export interface EmbeddableContext { embeddable: IEmbeddable; } -export interface ValueClickTriggerContext { - embeddable?: IEmbeddable; +export interface ValueClickTriggerContext { + embeddable?: T; timeFieldName?: string; data: { data: Array<{ @@ -39,8 +39,12 @@ export interface ValueClickTriggerContext { }; } -export interface RangeSelectTriggerContext { - embeddable?: IEmbeddable; +export const isValueClickTriggerContext = ( + context: ValueClickTriggerContext | RangeSelectTriggerContext +): context is ValueClickTriggerContext => context.data && 'data' in context.data; + +export interface RangeSelectTriggerContext { + embeddable?: T; timeFieldName?: string; data: { table: KibanaDatatable; @@ -49,6 +53,10 @@ export interface RangeSelectTriggerContext { }; } +export const isRangeSelectTriggerContext = ( + context: ValueClickTriggerContext | RangeSelectTriggerContext +): context is RangeSelectTriggerContext => context.data && 'range' in context.data; + export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { id: CONTEXT_MENU_TRIGGER, diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 2f139050e994ab..a1ef075d89d860 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -76,6 +76,7 @@ export { } from './state_sync'; export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; export { applyDiff } from './state_management/utils/diff_object'; +export { createStartServicesGetter } from './core/create_start_service_getter'; /** dummy plugin, we just want kibanaUtils to have its own bundle */ export function plugin() { 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 4d794618e85abe..c723388c021e98 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 14c21ae55dded5..e499c404ae7457 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -68,6 +68,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/src/plugins/ui_actions/public/util/configurable.ts b/src/plugins/ui_actions/public/util/configurable.ts index d3a527a2183b11..0a5a703abd0c18 100644 --- a/src/plugins/ui_actions/public/util/configurable.ts +++ b/src/plugins/ui_actions/public/util/configurable.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiComponent } from 'src/plugins/kibana_utils/common'; +import { UiComponent } from 'src/plugins/kibana_utils/public'; /** * Represents something that can be configured by user using UI. diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index b76ce141a44184..36a7674c47ab0c 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -104,16 +104,21 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide public async getDashboardIdFromCurrentUrl() { const currentUrl = await browser.getCurrentUrl(); - const urlSubstring = 'kibana#/dashboard/'; - const startOfIdIndex = currentUrl.indexOf(urlSubstring) + urlSubstring.length; - const endIndex = currentUrl.indexOf('?'); - const id = currentUrl.substring(startOfIdIndex, endIndex < 0 ? currentUrl.length : endIndex); + const id = this.getDashboardIdFromUrl(currentUrl); log.debug(`Dashboard id extracted from ${currentUrl} is ${id}`); return id; } + public getDashboardIdFromUrl(url: string) { + const urlSubstring = 'kibana#/dashboard/'; + const startOfIdIndex = url.indexOf(urlSubstring) + urlSubstring.length; + const endIndex = url.indexOf('?'); + const id = url.substring(startOfIdIndex, endIndex < 0 ? url.length : endIndex); + return id; + } + /** * Returns true if already on the dashboard landing page (that page doesn't have a link to itself). * @returns {Promise} @@ -512,6 +517,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.findByTestSubject( + 'embeddablePanelNotification-ACTION_PANEL_NOTIFICATIONS' + ); + 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 cc56714fcb2f84..f43d832b1edaec 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 bf8b82abf229c1..16f01cde784d9f 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 942b01e2b2f3ef..d2ef156c820236 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": false, "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/README.md b/x-pack/plugins/dashboard_enhanced/public/components/README.md deleted file mode 100644 index 8081f8a2451cfa..00000000000000 --- a/x-pack/plugins/dashboard_enhanced/public/components/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Presentation React components - -Here we keep reusable *presentation* (aka *dumb*) React components—these -components should not be connected to state and ideally should not know anything -about Kibana. diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts index e0f2aff7c70a82..75d25af2e0132e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -5,21 +5,26 @@ */ import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { SharePluginStart, SharePluginSetup } from '../../../../src/plugins/share/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { DashboardDrilldownsService } from './services'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { DrilldownsSetup, DrilldownsStart } from '../../drilldowns/public'; export interface SetupDependencies { drilldowns: DrilldownsSetup; embeddable: EmbeddableSetup; uiActions: UiActionsSetup; + share: SharePluginSetup; } export interface StartDependencies { drilldowns: DrilldownsStart; embeddable: EmbeddableStart; uiActions: UiActionsStart; + 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.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index dcdefba04d882b..83bbad9ae6b21a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -20,8 +20,8 @@ const drilldowns = drilldownsPluginMock.createStartContract(); const uiActions = uiActionsPluginMock.createStartContract(); const actionParams: OpenFlyoutAddDrilldownParams = { - drilldowns: () => Promise.resolve(drilldowns), - overlays: () => Promise.resolve(overlays), + drilldowns: () => drilldowns, + overlays: () => overlays, }; test('should create', () => { 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 d901bb8cf4e471..20eaba91786340 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 @@ -16,8 +16,8 @@ import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddabl export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN'; export interface OpenFlyoutAddDrilldownParams { - overlays: () => Promise; - drilldowns: () => Promise; + overlays: () => CoreStart['overlays']; + drilldowns: () => DrilldownsStart; } export class FlyoutCreateDrilldownAction implements ActionByType { @@ -50,8 +50,8 @@ export class FlyoutCreateDrilldownAction implements ActionByType Promise.resolve(drilldowns), - overlays: () => Promise.resolve(overlays), + drilldowns: () => drilldowns, + overlays: () => overlays, }; test('should create', () => { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 23a6689bd8d9dc..3a4eb4a4b86446 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -20,8 +20,8 @@ import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/publ export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN'; export interface FlyoutEditDrilldownParams { - overlays: () => Promise; - drilldowns: () => Promise; + overlays: () => CoreStart['overlays']; + drilldowns: () => DrilldownsStart; } export class FlyoutEditDrilldownAction implements ActionByType { @@ -48,8 +48,8 @@ export class FlyoutEditDrilldownAction implements ActionByType, + core: CoreSetup, plugins: SetupDependencies, { enableDrilldowns }: BootstrapParams ) { @@ -39,10 +40,17 @@ export class DashboardDrilldownsService { } } - setupDrilldowns(core: CoreSetup<{ drilldowns: DrilldownsStart }>, 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; + setupDrilldowns(core: CoreSetup, plugins: SetupDependencies) { + const getStartServices = createStartServicesGetter( + core.getStartServices + ); + + const overlays = () => getStartServices().core.overlays; + const drilldowns = () => getStartServices().plugins.drilldowns; + const getSavedObjectsClient = () => getStartServices().core.savedObjects.client; + const getApplicationService = () => getStartServices().core.application; + const getGetUrlGenerator = () => getStartServices().plugins.share.urlGenerators.getUrlGenerator; + const getDataPluginActions = () => getStartServices().plugins.data.actions; const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays, drilldowns }); plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown); @@ -51,7 +59,10 @@ export class DashboardDrilldownsService { plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown); const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({ - savedObjects, + getSavedObjectsClient, + getGetUrlGenerator, + getApplicationService, + 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 deleted file mode 100644 index e463cc38b6fbf7..00000000000000 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/collect_config.tsx +++ /dev/null @@ -1,55 +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. - */ - -import React, { useState, useEffect } from 'react'; -import { CollectConfigProps } from './types'; -import { DashboardDrilldownConfig } from '../../../components/dashboard_drilldown_config'; -import { Params } from './drilldown'; - -export interface CollectConfigContainerProps extends CollectConfigProps { - params: Params; -} - -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, - }) - } - onKeepRangeToggle={() => - onConfig({ - ...config, - useCurrentDateRange: !config.useCurrentDateRange, - }) - } - /> - ); -}; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx new file mode 100644 index 00000000000000..a75f459bbb100c --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx @@ -0,0 +1,171 @@ +/* + * 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 React from 'react'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { debounce, findIndex } from 'lodash'; +import { CoreStart, SimpleSavedObject } from '../../../../../../../../src/core/public'; +import { DashboardDrilldownConfig } from './dashboard_drilldown_config'; +import { txtDestinationDashboardNotFound } from './i18n'; +import { UiActionsCollectConfigProps } from '../../../../../../../../src/plugins/ui_actions/public'; +import { DrilldownFactoryContext } from '../../../../../../drilldowns/public'; +import { Config } from '../types'; +import { IEmbeddable } from '../../../../../../../../src/plugins/embeddable/public'; + +const mergeDashboards = ( + dashboards: Array>, + selectedDashboard?: EuiComboBoxOptionOption +) => { + // if we have a selected dashboard and its not in the list, append it + if (selectedDashboard && findIndex(dashboards, { value: selectedDashboard.value }) === -1) { + return [selectedDashboard, ...dashboards]; + } + return dashboards; +}; + +const dashboardSavedObjectToMenuItem = ( + savedObject: SimpleSavedObject<{ + title: string; + }> +) => ({ + value: savedObject.id, + label: savedObject.attributes.title, +}); + +interface CollectConfigProps extends UiActionsCollectConfigProps { + deps: { + getSavedObjectsClient: () => CoreStart['savedObjects']['client']; + }; + context: DrilldownFactoryContext<{ + embeddable: IEmbeddable; + }>; +} + +interface CollectConfigContainerState { + dashboards: Array>; + searchString?: string; + isLoading: boolean; + selectedDashboard?: EuiComboBoxOptionOption; + error?: string; +} + +export class CollectConfigContainer extends React.Component< + CollectConfigProps, + CollectConfigContainerState +> { + private isMounted = true; + state = { + dashboards: [], + isLoading: false, + searchString: undefined, + selectedDashboard: undefined, + error: undefined, + }; + + constructor(props: CollectConfigProps) { + super(props); + this.debouncedLoadDashboards = debounce(this.loadDashboards.bind(this), 500); + } + + componentDidMount() { + this.loadSelectedDashboard(); + this.loadDashboards(); + } + + componentWillUnmount() { + this.isMounted = false; + } + + render() { + const { config, onConfig } = this.props; + const { dashboards, selectedDashboard, isLoading, error } = this.state; + + return ( + { + onConfig({ ...config, dashboardId }); + if (this.state.error) { + this.setState({ error: undefined }); + } + }} + onSearchChange={this.debouncedLoadDashboards} + onCurrentFiltersToggle={() => + onConfig({ + ...config, + useCurrentFilters: !config.useCurrentFilters, + }) + } + onKeepRangeToggle={() => + onConfig({ + ...config, + useCurrentDateRange: !config.useCurrentDateRange, + }) + } + /> + ); + } + + private async loadSelectedDashboard() { + const { config } = this.props; + if (!config.dashboardId) return; + const savedObjectsClient = this.props.deps.getSavedObjectsClient(); + const savedObject = await savedObjectsClient.get<{ title: string }>( + 'dashboard', + config.dashboardId + ); + + if (!this.isMounted) return; + + // handle case when destination dashboard is no longer exist + if (savedObject.error?.statusCode === 404) { + this.setState({ + error: txtDestinationDashboardNotFound(config.dashboardId), + }); + this.props.onConfig({ ...config, dashboardId: undefined }); + return; + } + + if (savedObject.error) { + this.setState({ + error: savedObject.error.message, + }); + this.props.onConfig({ ...config, dashboardId: undefined }); + return; + } + + this.setState({ selectedDashboard: dashboardSavedObjectToMenuItem(savedObject) }); + } + + private readonly debouncedLoadDashboards: (searchString?: string) => void; + private async loadDashboards(searchString?: string) { + const currentDashboardId = this.props.context.placeContext.embeddable?.parent?.id; + this.setState({ searchString, isLoading: true }); + const savedObjectsClient = this.props.deps.getSavedObjectsClient(); + const { savedObjects } = await savedObjectsClient.find<{ title: string }>({ + type: 'dashboard', + search: searchString ? `${searchString}*` : undefined, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + perPage: 100, + }); + + // bail out if this response is no longer needed + 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 }); + } +} 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/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx similarity index 76% rename from x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx index 8e204b044a1368..f3a966a73509c2 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx @@ -8,12 +8,12 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; -import { DashboardDrilldownConfig } from '.'; +import { DashboardDrilldownConfig } from './dashboard_drilldown_config'; 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,16 +30,23 @@ const InteractiveDemo: React.FC = () => { onDashboardSelect={id => setActiveDashboardId(id)} onCurrentFiltersToggle={() => setCurrentFilters(old => !old)} onKeepRangeToggle={() => setKeepRange(old => !old)} + onSearchChange={() => {}} + isLoading={false} /> ); }; -storiesOf('components/DashboardDrilldownConfig', module) +storiesOf( + 'services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config', + module +) .add('default', () => ( console.log('onDashboardSelect', e)} + onSearchChange={() => {}} + isLoading={false} /> )) .add('with switches', () => ( @@ -49,6 +56,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/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx similarity index 52% rename from x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx index 911ff6f6326357..edeb7de48d9acf 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx @@ -4,8 +4,7 @@ * 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'); +// Need to wait for https://github.com/elastic/eui/pull/3173/ +// to unit test this component +// basic interaction is covered in end-to-end tests +test.todo(''); diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx similarity index 52% rename from x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx index b45ba602b9bb1b..a41a5fb718219d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx @@ -5,22 +5,24 @@ */ 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; + error?: string; } export const DashboardDrilldownConfig: React.FC = ({ @@ -31,24 +33,35 @@ export const DashboardDrilldownConfig: React.FC = onDashboardSelect, onCurrentFiltersToggle, onKeepRangeToggle, + onSearchChange, + isLoading, + error, }) => { - // 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'} + isInvalid={!!error} /> {!!onCurrentFiltersToggle && ( @@ -58,7 +71,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/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts similarity index 54% rename from x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/i18n.ts rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts index 38fe6dd1508536..a37f2bfa01bd49 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/i18n.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/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 filters and query from origin dashboard', + } +); + +export const txtUseCurrentDateRange = i18n.translate( + 'xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange', + { + defaultMessage: 'Use date range from origin dashboard', + } +); diff --git a/x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts similarity index 100% rename from x-pack/plugins/dashboard_enhanced/public/components/dashboard_drilldown_config/index.ts rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts new file mode 100644 index 00000000000000..6f6f7412f6b538 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtDestinationDashboardNotFound = (dashboardId?: string) => + i18n.translate('xpack.dashboard.drilldown.errorDestinationDashboardIsMissing', { + defaultMessage: + "Destination dashboard ('{dashboardId}') no longer exists. Choose another dashboard.", + values: { + dashboardId, + }, + }); diff --git a/x-pack/plugins/dashboard_enhanced/public/components/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts similarity index 77% rename from x-pack/plugins/dashboard_enhanced/public/components/index.ts rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts index b9a64a3cc17e6e..c34290528d914b 100644 --- a/x-pack/plugins/dashboard_enhanced/public/components/index.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './dashboard_drilldown_config'; +export { CollectConfigContainer } from './collect_config_container'; 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 0fb60bb1064a1b..8b69b20cac3e38 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,335 @@ * 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'; +import { + RangeSelectTriggerContext, + ValueClickTriggerContext, +} from '../../../../../../../src/plugins/embeddable/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)); +}); + +test('initial config: switches are ON', () => { + const drilldown = new DashboardToDashboardDrilldown({} as any); + const { useCurrentDateRange, useCurrentFilters } = drilldown.createConfig(); + expect(useCurrentDateRange).toBe(true); + expect(useCurrentFilters).toBe(true); }); -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' - ); +test('getHref is defined', () => { + const drilldown = new DashboardToDashboardDrilldown({} as any); + expect(drilldown.getHref).toBeDefined(); }); + +describe('.execute() & getHref', () => { + /** + * 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: Filter[], + useRangeEvent = false + ) { + const navigateToApp = jest.fn(); + const getUrlForApp = jest.fn((app, opt) => `${app}/${opt.path}`); + const dataPluginActions = dataPluginMock.createStartContract().actions; + const savedObjectsClient = savedObjectsServiceMock.createStartContract().client; + + const drilldown = new DashboardToDashboardDrilldown({ + getApplicationService: () => ({ + navigateToApp, + getUrlForApp, + }), + getGetUrlGenerator: () => () => + createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ appBasePath: 'test', useHashedUrl: false }) + ) as UrlGeneratorContract, + getDataPluginActions: () => dataPluginActions, + getSavedObjectsClient: () => savedObjectsClient, + }); + const selectRangeFiltersSpy = jest + .spyOn(dataPluginActions, 'createFiltersFromRangeSelectAction') + .mockImplementation(() => Promise.resolve(filtersFromEvent)); + const valueClickFiltersSpy = jest + .spyOn(dataPluginActions, 'createFiltersFromValueClickAction') + .mockImplementation(() => Promise.resolve(filtersFromEvent)); + + const completeConfig: Config = { + dashboardId: 'id', + useCurrentFilters: false, + useCurrentDateRange: false, + ...config, + }; + + const context = ({ + data: useRangeEvent + ? ({ range: {} } as RangeSelectTriggerContext['data']) + : ({ data: [] } as ValueClickTriggerContext['data']), + timeFieldName: 'order_date', + embeddable: { + getInput: () => ({ + filters: [], + timeRange: { from: 'now-15m', to: 'now' }, + query: { query: 'test', language: 'kuery' }, + ...embeddableInput, + }), + }, + } as unknown) as ActionContext; + + await drilldown.execute(completeConfig, context); + + 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'); + + const executeNavigatedPath = navigateToApp.mock.calls[0][1]?.path; + const href = await drilldown.getHref(completeConfig, context); + + expect(href.includes(executeNavigatedPath)).toBe(true); + + return { + href, + }; + } + + test('navigates to correct dashboard', async () => { + const testDashboardId = 'dashboardId'; + const { href } = await setupTestBed( + { + dashboardId: testDashboardId, + }, + {}, + [], + false + ); + + expect(href).toEqual(expect.stringContaining(`dashboard/${testDashboardId}`)); + }); + + test('query is removed if filters are disabled', async () => { + const queryString = 'querystring'; + const queryLanguage = 'kuery'; + const { href } = await setupTestBed( + { + useCurrentFilters: false, + }, + { + query: { query: queryString, language: queryLanguage }, + }, + [] + ); + + expect(href).toEqual(expect.not.stringContaining(queryString)); + expect(href).toEqual(expect.not.stringContaining(queryLanguage)); + }); + + test('navigates with query if filters are enabled', async () => { + const queryString = 'querystring'; + const queryLanguage = 'kuery'; + const { href } = await setupTestBed( + { + useCurrentFilters: true, + }, + { + query: { query: queryString, language: queryLanguage }, + }, + [] + ); + + expect(href).toEqual(expect.stringContaining(queryString)); + expect(href).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 { href } = await setupTestBed( + { + useCurrentFilters: true, + }, + { + filters: [getFilter(false, existingAppFilterKey), getFilter(true, existingGlobalFilterKey)], + }, + [getFilter(false, newAppliedFilterKey)] + ); + + expect(href).toEqual(expect.stringContaining(existingAppFilterKey)); + expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(href).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 { href } = await setupTestBed( + { + useCurrentFilters: false, + }, + { + filters: [getFilter(false, existingAppFilterKey), getFilter(true, existingGlobalFilterKey)], + }, + [getFilter(false, newAppliedFilterKey)] + ); + + expect(href).not.toEqual(expect.stringContaining(existingAppFilterKey)); + expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(href).toEqual(expect.stringContaining(newAppliedFilterKey)); + }); + + test('when user chooses to keep current time range, current time range is passed in url', async () => { + const { href } = await setupTestBed( + { + useCurrentDateRange: true, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + [] + ); + + expect(href).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 { href } = await setupTestBed( + { + useCurrentDateRange: false, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + [], + false + ); + + expect(href).not.toEqual(expect.stringContaining('now-300m')); + }); + + test('if range filter contains date, then it is passed as time', async () => { + const { href } = await setupTestBed( + { + useCurrentDateRange: true, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + [getMockTimeRangeFilter()], + true + ); + + expect(href).not.toEqual(expect.stringContaining('now-300m')); + expect(href).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 9d2a378f08acd7..68aac6c2bd360e 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,19 +7,32 @@ import React from 'react'; import { CoreStart } from 'src/core/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; -import { PlaceContext, ActionContext, Config, CollectConfigProps } from './types'; -import { CollectConfigContainer } from './collect_config'; +import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; +import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; +import { PlaceContext, ActionContext, Config } from './types'; +import { CollectConfigContainer } from './components'; 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'; +import { + isRangeSelectTriggerContext, + isValueClickTriggerContext, +} from '../../../../../../../src/plugins/embeddable/public'; export interface Params { - savedObjects: () => Promise; + getSavedObjectsClient: () => CoreStart['savedObjects']['client']; + getApplicationService: () => Pick; + getGetUrlGenerator: () => SharePluginStart['urlGenerators']['getUrlGenerator']; + getDataPluginActions: () => DataPublicPluginStart['actions']; } export class DashboardToDashboardDrilldown - implements Drilldown { - constructor(protected readonly params: Params) {} + implements Drilldown> { + constructor(protected readonly params: Params) { + this.getDestinationUrl = this.getDestinationUrl.bind(this); + } public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN; @@ -29,14 +42,14 @@ export class DashboardToDashboardDrilldown public readonly euiIcon = 'dashboardApp'; - private readonly ReactCollectConfig: React.FC = props => ( - + private readonly ReactCollectConfig: React.FC = props => ( + ); public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); public readonly createConfig = () => ({ - dashboardId: '123', + dashboardId: '', useCurrentFilters: true, useCurrentDateRange: true, }); @@ -46,7 +59,105 @@ export class DashboardToDashboardDrilldown return true; }; - public readonly execute = () => { - alert('Go to another dashboard!'); + public readonly getHref = async ( + config: Config, + context: ActionContext + ): Promise => { + const { getUrlForApp } = await this.params.getApplicationService(); + const dashboardPath = await this.getDestinationUrl(config, context); + // note: extracting hash and using 'kibana' as appId will be redundant, + // when dashboard move to np urls. (urlGenerator generates np url, which is not supported yet) + const dashboardHash = dashboardPath.split('#')[1]; + return getUrlForApp('kibana', { + path: `#${dashboardHash}`, + }); }; + + public readonly execute = async ( + config: Config, + context: ActionContext + ) => { + const { navigateToApp } = await this.params.getApplicationService(); + const dashboardPath = await this.getDestinationUrl(config, context); + // note: extracting hash and using 'kibana' as appId will be redundant, + // when dashboard move to np urls. (urlGenerator generates np url, which is not supported yet) + const dashboardHash = dashboardPath.split('#')[1]; + await navigateToApp('kibana', { + path: `#${dashboardHash}`, + }); + }; + + private async getDestinationUrl( + config: Config, + context: ActionContext + ): Promise { + const getUrlGenerator = await this.params.getGetUrlGenerator(); + + const { + createFiltersFromRangeSelectAction, + createFiltersFromValueClickAction, + } = 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 existingFilters = + (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; + let filtersFromEvent = await (async () => { + try { + if (isRangeSelectTriggerContext(context)) + return await createFiltersFromRangeSelectAction(context.data); + if (isValueClickTriggerContext(context)) + return await createFiltersFromValueClickAction(context.data); + + // eslint-disable-next-line no-console + console.warn( + ` + DashboardToDashboard drilldown: can't extract filters from action. + Is it not supported action?`, + context + ); + + return []; + } catch (e) { + // eslint-disable-next-line no-console + console.warn( + ` + DashboardToDashboard drilldown: error extracting filters from action. + Continuing without applying filters from event`, + e + ); + return []; + } + })(); + + if (context.timeFieldName) { + const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( + context.timeFieldName, + filtersFromEvent + ); + filtersFromEvent = restOfFilters; + if (timeRangeFilter) { + timeRange = esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter); + } + } + + return getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({ + dashboardId: config.dashboardId, + query: config.useCurrentFilters ? query : undefined, + timeRange, + filters: [...existingFilters, ...filtersFromEvent], + }); + } } 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 28200d45b4daf2..530d48e216530a 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 @@ -5,19 +5,19 @@ */ import { - RangeSelectTriggerContext, - ValueClickTriggerContext, EmbeddableContext, + ValueClickTriggerContext, + RangeSelectTriggerContext, + IEmbeddable, } from '../../../../../../../src/plugins/embeddable/public'; -import { UiActionsCollectConfigProps } from '../../../../../../../src/plugins/ui_actions/public'; export type PlaceContext = EmbeddableContext; -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; +export type ActionContext = + | ValueClickTriggerContext + | RangeSelectTriggerContext; export interface Config { dashboardId?: string; useCurrentFilters: boolean; useCurrentDateRange: boolean; } - -export type CollectConfigProps = UiActionsCollectConfigProps; diff --git a/x-pack/plugins/dashboard_enhanced/scripts/storybook.js b/x-pack/plugins/dashboard_enhanced/scripts/storybook.js index f2cbe4135f4cb6..5d95c56c31e3b1 100644 --- a/x-pack/plugins/dashboard_enhanced/scripts/storybook.js +++ b/x-pack/plugins/dashboard_enhanced/scripts/storybook.js @@ -9,5 +9,5 @@ import { join } from 'path'; // eslint-disable-next-line require('@kbn/storybook').runStorybookCli({ name: 'dashboard_enhanced', - storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')], + storyGlobs: [join(__dirname, '..', 'public', '**', '*.story.tsx')], }); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index c8a67a20aa5c16..f049b231c15cae 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -22,7 +22,7 @@ import { SELECT_RANGE_TRIGGER, TriggerContextMapping, } from '../../../../../../src/plugins/ui_actions/public'; -import { useContainerState } from '../../../../../../src/plugins/kibana_utils/common'; +import { useContainerState } from '../../../../../../src/plugins/kibana_utils/public'; import { DrilldownListItem } from '../list_manage_drilldowns'; import { toastDrilldownCreated, 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 70f4d735e2a749..31384860786efe 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 before testing.', 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 before testing.', values: { drilldownName, }, @@ -48,7 +48,7 @@ export const toastDrilldownDeleted = { text: i18n.translate( 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText', { - defaultMessage: 'You deleted a drilldown', + defaultMessage: 'You deleted a drilldown.', } ), }; 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 8c6739a8ad6c8a..48e17dadc810f0 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 faa965a98a4bb4..8541aae06ff0c7 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 4560773cc8a6dd..d9c53ae6f737a4 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 bdafaaf07873c5..93b3710bf6cc66 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 5a15781a1faf2a..ab51c0a829ed35 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 044e29c671de40..17ccd60e17ce4d 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 00000000000000..1a90d5d1fe52a7 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts @@ -0,0 +1,176 @@ +/* + * 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(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); + + // 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: -100, + y: 0, + }, + }, + { + location: areaChart, + offset: { + x: 100, + y: 0, + }, + } + ); + } + + 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 00000000000000..ab273018dc3f78 --- /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 794bd2f1f0db36..d6ce16167d1744 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); + 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 00000000000000..febf312799eee0 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 00000000000000..210fade40c648f --- /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 00000000000000..1710cb8bfb71a6 --- /dev/null +++ b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts @@ -0,0 +1,95 @@ +/* + * 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 = 'createDrilldownFlyout'; +const MANAGE_DRILLDOWNS_FLYOUT_DATA_TEST_SUBJ = 'editDrilldownFlyout'; +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.findByTestSubject('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 95101605ce4682..dee525fa0a388b 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 00000000000000..febcbdcc9273e3 --- /dev/null +++ b/x-pack/test/functional/services/dashboard/panel_drilldown_actions.ts @@ -0,0 +1,80 @@ +/* + * 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'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + +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}"`); + (await this.getActionWebElementByText(text)).click(); + } + + async getActionHrefByText(text: string) { + log.debug(`getActionHref: "${text}"`); + const item = await this.getActionWebElementByText(text); + return item.getAttribute('href'); + } + + async getActionWebElementByText(text: string): Promise { + log.debug(`getActionWebElement: "${text}"`); + const menu = await testSubjects.find('multipleActionsContextMenu'); + const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); + for (const item of items) { + const currentText = await item.getVisibleText(); + if (currentText === text) { + return item; + } + } + + throw new Error(`No action matching text "${text}"`); + } + })(); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index aec91ba9e90341..f1d84f3054aa08 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, };