mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
"Explore underlying data" in-chart action (#69494)
* refactor: 💡 rename folder to "explore_data" * style: 💄 check for "share" plugin in more semantic way "explore data" actions use Discover URL generator, which is registered in "share" plugin, which is optional plugin, so we check for its existance, because otherwise URL generator is not available. * refactor: 💡 move KibanaURL to a separate file * feat: 🎸 add "Explore underlying data" in-chart action * fix: 🐛 fix imports after refactor * feat: 🎸 add start.filtersFromContext to embeddable plugin * feat: 🎸 add type checkers to data plugin * feat: 🎸 better handle empty filters in Discover URL generator * feat: 🎸 implement .getUrl() method of explore data in-chart act * feat: 🎸 add embeddable.filtersAndTimeRangeFromContext() * feat: 🎸 improve getUrl() method of explore data action * test: 💍 update test mock * fix possible stale hashHistory.location in discover * style: 💄 ensureHashHistoryLocation -> syncHistoryLocations * docs: ✏️ update autogenerated docs * test: 💍 add in-chart "Explore underlying data" unit tests * test: 💍 add in-chart "Explore underlying data" functional tests * test: 💍 clean-up custom time range after panel action tests * chore: 🤖 fix embeddable plugin mocks * chore: 🤖 fix another mock * test: 💍 add support for new action to pie chart service Co-authored-by: Anton Dosov <anton.dosov@elastic.co>
This commit is contained in:
parent
b3b5dab00d
commit
684aa68f17
41 changed files with 997 additions and 197 deletions
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilter](./kibana-plugin-plugins-data-public.isfilter.md)
|
||||
|
||||
## isFilter variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isFilter: (x: unknown) => x is Filter
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilters](./kibana-plugin-plugins-data-public.isfilters.md)
|
||||
|
||||
## isFilters variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isFilters: (x: unknown) => x is Filter[]
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isQuery](./kibana-plugin-plugins-data-public.isquery.md)
|
||||
|
||||
## isQuery variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isQuery: (x: unknown) => x is Query
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md)
|
||||
|
||||
## isTimeRange variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isTimeRange: (x: unknown) => x is TimeRange
|
||||
```
|
|
@ -110,6 +110,10 @@
|
|||
| [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {<!-- -->Array<string>} |
|
||||
| [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | |
|
||||
| [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | |
|
||||
| [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) | |
|
||||
| [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) | |
|
||||
| [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | |
|
||||
| [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | |
|
||||
| [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | |
|
||||
| [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | |
|
||||
| [search](./kibana-plugin-plugins-data-public.search.md) | |
|
||||
|
|
|
@ -107,3 +107,13 @@ export const pinFilter = (filter: Filter) =>
|
|||
|
||||
export const unpinFilter = (filter: Filter) =>
|
||||
!isFilterPinned(filter) ? filter : toggleFilterPinned(filter);
|
||||
|
||||
export const isFilter = (x: unknown): x is Filter =>
|
||||
!!x &&
|
||||
typeof x === 'object' &&
|
||||
!!(x as Filter).meta &&
|
||||
typeof (x as Filter).meta === 'object' &&
|
||||
typeof (x as Filter).meta.disabled === 'boolean';
|
||||
|
||||
export const isFilters = (x: unknown): x is Filter[] =>
|
||||
Array.isArray(x) && !x.find((y) => !isFilter(y));
|
||||
|
|
|
@ -20,11 +20,12 @@
|
|||
export * from './constants';
|
||||
export * from './es_query';
|
||||
export * from './field_formats';
|
||||
export * from './field_mapping';
|
||||
export * from './index_patterns';
|
||||
export * from './kbn_field_types';
|
||||
export * from './query';
|
||||
export * from './search';
|
||||
export * from './search/aggs';
|
||||
export * from './timefilter';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './field_mapping';
|
||||
|
|
|
@ -19,3 +19,4 @@
|
|||
|
||||
export * from './filter_manager';
|
||||
export * from './types';
|
||||
export * from './is_query';
|
||||
|
|
27
src/plugins/data/common/query/is_query.ts
Normal file
27
src/plugins/data/common/query/is_query.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Query } from './types';
|
||||
|
||||
export const isQuery = (x: unknown): x is Query =>
|
||||
!!x &&
|
||||
typeof x === 'object' &&
|
||||
typeof (x as Query).language === 'string' &&
|
||||
(typeof (x as Query).query === 'string' ||
|
||||
(typeof (x as Query).query === 'object' && !!(x as Query).query));
|
20
src/plugins/data/common/timefilter/index.ts
Normal file
20
src/plugins/data/common/timefilter/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { isTimeRange } from './is_time_range';
|
26
src/plugins/data/common/timefilter/is_time_range.ts
Normal file
26
src/plugins/data/common/timefilter/is_time_range.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeRange } from './types';
|
||||
|
||||
export const isTimeRange = (x: unknown): x is TimeRange =>
|
||||
!!x &&
|
||||
typeof x === 'object' &&
|
||||
typeof (x as TimeRange).from === 'string' &&
|
||||
typeof (x as TimeRange).to === 'string';
|
|
@ -440,6 +440,8 @@ export {
|
|||
getKbnTypeNames,
|
||||
} from '../common';
|
||||
|
||||
export { isTimeRange, isQuery, isFilter, isFilters } from '../common';
|
||||
|
||||
export * from '../common/field_mapping';
|
||||
|
||||
/*
|
||||
|
|
|
@ -1291,6 +1291,26 @@ export interface ISearchStrategy<T extends TStrategyTypes> {
|
|||
search: ISearch<T>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const isFilter: (x: unknown) => x is Filter;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const isFilters: (x: unknown) => x is Filter[];
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const isQuery: (x: unknown) => x is Query;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isTimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const isTimeRange: (x: unknown) => x is TimeRange;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ISyncSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -27,4 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
|
||||
export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
|
||||
export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
|
||||
export { DISCOVER_APP_URL_GENERATOR } from './url_generator';
|
||||
export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator';
|
||||
|
|
|
@ -60,10 +60,23 @@ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter<Doc
|
|||
'DocViewsRegistry'
|
||||
);
|
||||
/**
|
||||
* Makes sure discover and context are using one instance of history
|
||||
* Makes sure discover and context are using one instance of history.
|
||||
*/
|
||||
export const getHistory = _.once(() => createHashHistory());
|
||||
|
||||
/**
|
||||
* Discover currently uses two `history` instances: one from Kibana Platform and
|
||||
* another from `history` package. Below function is used every time Discover
|
||||
* app is loaded to synchronize both instances.
|
||||
*
|
||||
* This helper is temporary until https://github.com/elastic/kibana/issues/65161 is resolved.
|
||||
*/
|
||||
export const syncHistoryLocations = () => {
|
||||
const h = getHistory();
|
||||
Object.assign(h.location, createHashHistory().location);
|
||||
return h;
|
||||
};
|
||||
|
||||
export const [getScopedHistory, setScopedHistory] = createGetterSetter<ScopedHistory>(
|
||||
'scopedHistory'
|
||||
);
|
||||
|
|
|
@ -55,6 +55,7 @@ import {
|
|||
setServices,
|
||||
setScopedHistory,
|
||||
getScopedHistory,
|
||||
syncHistoryLocations,
|
||||
getServices,
|
||||
} from './kibana_services';
|
||||
import { createSavedSearchesLoader } from './saved_searches';
|
||||
|
@ -245,6 +246,7 @@ export class DiscoverPlugin
|
|||
throw Error('Discover plugin method initializeInnerAngular is undefined');
|
||||
}
|
||||
setScopedHistory(params.history);
|
||||
syncHistoryLocations();
|
||||
appMounted();
|
||||
const {
|
||||
plugins: { data: dataStart },
|
||||
|
|
|
@ -98,11 +98,13 @@ export class DiscoverUrlGenerator
|
|||
const queryState: QueryState = {};
|
||||
|
||||
if (query) appState.query = query;
|
||||
if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
|
||||
if (filters && filters.length)
|
||||
appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
|
||||
if (indexPatternId) appState.index = indexPatternId;
|
||||
|
||||
if (timeRange) queryState.time = timeRange;
|
||||
if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
|
||||
if (filters && filters.length)
|
||||
queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
|
||||
if (refreshInterval) queryState.refreshInterval = refreshInterval;
|
||||
|
||||
let url = `${this.params.appBasePath}#/${savedSearchPath}`;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"inspector",
|
||||
"uiActions"
|
||||
],
|
||||
|
|
|
@ -28,6 +28,7 @@ export {
|
|||
ACTION_EDIT_PANEL,
|
||||
Adapters,
|
||||
AddPanelAction,
|
||||
ChartActionContext,
|
||||
Container,
|
||||
ContainerInput,
|
||||
ContainerOutput,
|
||||
|
|
|
@ -39,10 +39,6 @@ export interface ValueClickTriggerContext<T extends IEmbeddable = IEmbeddable> {
|
|||
};
|
||||
}
|
||||
|
||||
export const isValueClickTriggerContext = (
|
||||
context: ValueClickTriggerContext | RangeSelectTriggerContext
|
||||
): context is ValueClickTriggerContext => context.data && 'data' in context.data;
|
||||
|
||||
export interface RangeSelectTriggerContext<T extends IEmbeddable = IEmbeddable> {
|
||||
embeddable?: T;
|
||||
data: {
|
||||
|
@ -53,8 +49,16 @@ export interface RangeSelectTriggerContext<T extends IEmbeddable = IEmbeddable>
|
|||
};
|
||||
}
|
||||
|
||||
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> =
|
||||
| ValueClickTriggerContext<T>
|
||||
| RangeSelectTriggerContext<T>;
|
||||
|
||||
export const isValueClickTriggerContext = (
|
||||
context: ChartActionContext
|
||||
): context is ValueClickTriggerContext => context.data && 'data' in context.data;
|
||||
|
||||
export const isRangeSelectTriggerContext = (
|
||||
context: ValueClickTriggerContext | RangeSelectTriggerContext
|
||||
context: ChartActionContext
|
||||
): context is RangeSelectTriggerContext => context.data && 'range' in context.data;
|
||||
|
||||
export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER';
|
||||
|
|
|
@ -31,6 +31,7 @@ import { coreMock } from '../../../core/public/mocks';
|
|||
import { UiActionsService } from './lib/ui_actions';
|
||||
import { CoreStart } from '../../../core/public';
|
||||
import { Start as InspectorStart } from '../../inspector/public';
|
||||
import { dataPluginMock } from '../../data/public/mocks';
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { inspectorPluginMock } from '../../inspector/public/mocks';
|
||||
|
@ -100,6 +101,8 @@ const createStartContract = (): Start => {
|
|||
EmbeddablePanel: jest.fn(),
|
||||
getEmbeddablePanel: jest.fn(),
|
||||
getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
|
||||
filtersAndTimeRangeFromContext: jest.fn(),
|
||||
filtersFromContext: jest.fn(),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
@ -108,11 +111,13 @@ const createInstance = (setupPlugins: Partial<EmbeddableSetupDependencies> = {})
|
|||
const plugin = new EmbeddablePublicPlugin({} as any);
|
||||
const setup = plugin.setup(coreMock.createSetup(), {
|
||||
uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(),
|
||||
data: dataPluginMock.createSetupContract(),
|
||||
});
|
||||
const doStart = (startPlugins: Partial<EmbeddableStartDependencies> = {}) =>
|
||||
plugin.start(coreMock.createStart(), {
|
||||
uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(),
|
||||
inspector: inspectorPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
});
|
||||
return {
|
||||
plugin,
|
||||
|
|
|
@ -17,6 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
DataPublicPluginSetup,
|
||||
DataPublicPluginStart,
|
||||
Filter,
|
||||
TimeRange,
|
||||
esFilters,
|
||||
} from '../../data/public';
|
||||
import { getSavedObjectFinder } from '../../saved_objects/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public';
|
||||
import { Start as InspectorStart } from '../../inspector/public';
|
||||
|
@ -36,15 +43,20 @@ import {
|
|||
defaultEmbeddableFactoryProvider,
|
||||
IEmbeddable,
|
||||
EmbeddablePanel,
|
||||
ChartActionContext,
|
||||
isRangeSelectTriggerContext,
|
||||
isValueClickTriggerContext,
|
||||
} from './lib';
|
||||
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
|
||||
import { EmbeddableStateTransfer } from './lib/state_transfer';
|
||||
|
||||
export interface EmbeddableSetupDependencies {
|
||||
data: DataPublicPluginSetup;
|
||||
uiActions: UiActionsSetup;
|
||||
}
|
||||
|
||||
export interface EmbeddableStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
uiActions: UiActionsStart;
|
||||
inspector: InspectorStart;
|
||||
}
|
||||
|
@ -70,6 +82,19 @@ export interface EmbeddableStart {
|
|||
embeddableFactoryId: string
|
||||
) => EmbeddableFactory<I, O, E> | undefined;
|
||||
getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
|
||||
|
||||
/**
|
||||
* Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries.
|
||||
*/
|
||||
filtersFromContext: (context: ChartActionContext) => Promise<Filter[]>;
|
||||
|
||||
/**
|
||||
* Returns possible time range and filters that can be constructed from {@link ChartActionContext} object.
|
||||
*/
|
||||
filtersAndTimeRangeFromContext: (
|
||||
context: ChartActionContext
|
||||
) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>;
|
||||
|
||||
EmbeddablePanel: EmbeddablePanelHOC;
|
||||
getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC;
|
||||
getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer;
|
||||
|
@ -107,7 +132,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
{ uiActions, inspector }: EmbeddableStartDependencies
|
||||
{ data, uiActions, inspector }: EmbeddableStartDependencies
|
||||
): EmbeddableStart {
|
||||
this.embeddableFactoryDefinitions.forEach((def) => {
|
||||
this.embeddableFactories.set(
|
||||
|
@ -121,6 +146,41 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
this.outgoingOnlyStateTransfer = new EmbeddableStateTransfer(core.application.navigateToApp);
|
||||
this.isRegistryReady = true;
|
||||
|
||||
const filtersFromContext: EmbeddableStart['filtersFromContext'] = async (context) => {
|
||||
try {
|
||||
if (isRangeSelectTriggerContext(context))
|
||||
return await data.actions.createFiltersFromRangeSelectAction(context.data);
|
||||
if (isValueClickTriggerContext(context))
|
||||
return await data.actions.createFiltersFromValueClickAction(context.data);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Can't extract filters from action.", context);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Error extracting filters from action. Returning empty filter list.', error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async (
|
||||
context
|
||||
) => {
|
||||
const filters = await filtersFromContext(context);
|
||||
|
||||
if (!context.data.timeFieldName) return { filters };
|
||||
|
||||
const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
|
||||
context.data.timeFieldName,
|
||||
filters
|
||||
);
|
||||
|
||||
return {
|
||||
filters: restOfFilters,
|
||||
timeRange: timeRangeFilter
|
||||
? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({
|
||||
embeddable,
|
||||
hideHeader,
|
||||
|
@ -146,6 +206,8 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
return {
|
||||
getEmbeddableFactory: this.getEmbeddableFactory,
|
||||
getEmbeddableFactories: this.getEmbeddableFactories,
|
||||
filtersFromContext,
|
||||
filtersAndTimeRangeFromContext,
|
||||
getStateTransfer: (history?: ScopedHistory) => {
|
||||
return history
|
||||
? new EmbeddableStateTransfer(core.application.navigateToApp, history)
|
||||
|
|
|
@ -23,6 +23,7 @@ import { UiActionsStart } from '../../../ui_actions/public';
|
|||
import { uiActionsPluginMock } from '../../../ui_actions/public/mocks';
|
||||
// eslint-disable-next-line
|
||||
import { inspectorPluginMock } from '../../../inspector/public/mocks';
|
||||
import { dataPluginMock } from '../../../data/public/mocks';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin';
|
||||
|
||||
|
@ -42,7 +43,10 @@ export const testPlugin = (
|
|||
const uiActions = uiActionsPluginMock.createPlugin(coreSetup, coreStart);
|
||||
const initializerContext = {} as any;
|
||||
const plugin = new EmbeddablePublicPlugin(initializerContext);
|
||||
const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup });
|
||||
const setup = plugin.setup(coreSetup, {
|
||||
data: dataPluginMock.createSetupContract(),
|
||||
uiActions: uiActions.setup,
|
||||
});
|
||||
|
||||
return {
|
||||
plugin,
|
||||
|
@ -51,8 +55,9 @@ export const testPlugin = (
|
|||
setup,
|
||||
doStart: (anotherCoreStart: CoreStart = coreStart) => {
|
||||
const start = plugin.start(anotherCoreStart, {
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
inspector: inspectorPluginMock.createStartContract(),
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
});
|
||||
return start;
|
||||
},
|
||||
|
|
|
@ -213,5 +213,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft
|
|||
await testSubjects.click('saveNewTitleButton');
|
||||
await this.toggleContextMenu(panel);
|
||||
}
|
||||
|
||||
async getActionWebElementByText(text: string): Promise<WebElementWrapper> {
|
||||
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}"`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -28,10 +28,13 @@ export function PieChartProvider({ getService }: FtrProviderContext) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const find = getService('find');
|
||||
const defaultFindTimeout = config.get('timeouts.find');
|
||||
const panelActions = getService('dashboardPanelActions');
|
||||
|
||||
return new (class PieChart {
|
||||
async filterOnPieSlice(name?: string) {
|
||||
log.debug(`PieChart.filterOnPieSlice(${name})`);
|
||||
private readonly filterActionText = 'Apply filter to current view';
|
||||
|
||||
async clickOnPieSlice(name?: string) {
|
||||
log.debug(`PieChart.clickOnPieSlice(${name})`);
|
||||
if (name) {
|
||||
await testSubjects.click(`pieSlice-${name.split(' ').join('-')}`);
|
||||
} else {
|
||||
|
@ -44,6 +47,16 @@ export function PieChartProvider({ getService }: FtrProviderContext) {
|
|||
}
|
||||
}
|
||||
|
||||
async filterOnPieSlice(name?: string) {
|
||||
log.debug(`PieChart.filterOnPieSlice(${name})`);
|
||||
await this.clickOnPieSlice(name);
|
||||
const hasUiActionsPopup = await testSubjects.exists('multipleActionsContextMenu');
|
||||
if (hasUiActionsPopup) {
|
||||
const actionElement = await panelActions.getActionWebElementByText(this.filterActionText);
|
||||
await actionElement.click();
|
||||
}
|
||||
}
|
||||
|
||||
async filterByLegendItem(label: string) {
|
||||
log.debug(`PieChart.filterByLegendItem(${label})`);
|
||||
await testSubjects.click(`legend-${label}`);
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DiscoverStart } from '../../../../../../src/plugins/discover/public';
|
||||
import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public';
|
||||
import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
|
||||
import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
import { CoreStart } from '../../../../../../src/core/public';
|
||||
import { KibanaURL } from './kibana_url';
|
||||
import * as shared from './shared';
|
||||
|
||||
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
|
||||
|
||||
export interface PluginDeps {
|
||||
discover: Pick<DiscoverStart, 'urlGenerator'>;
|
||||
embeddable: Pick<EmbeddableStart, 'filtersAndTimeRangeFromContext'>;
|
||||
}
|
||||
|
||||
export interface CoreDeps {
|
||||
application: Pick<CoreStart['application'], 'navigateToApp'>;
|
||||
}
|
||||
|
||||
export interface Params {
|
||||
start: StartServicesGetter<PluginDeps, unknown, CoreDeps>;
|
||||
}
|
||||
|
||||
export abstract class AbstractExploreDataAction<Context extends { embeddable?: IEmbeddable }> {
|
||||
public readonly getIconType = (context: Context): string => 'discoverApp';
|
||||
|
||||
public readonly getDisplayName = (context: Context): string =>
|
||||
i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', {
|
||||
defaultMessage: 'Explore underlying data',
|
||||
});
|
||||
|
||||
constructor(protected readonly params: Params) {}
|
||||
|
||||
protected abstract async getUrl(context: Context): Promise<KibanaURL>;
|
||||
|
||||
public async isCompatible({ embeddable }: Context): Promise<boolean> {
|
||||
if (!embeddable) return false;
|
||||
if (!this.params.start().plugins.discover.urlGenerator) return false;
|
||||
if (!shared.isVisualizeEmbeddable(embeddable)) return false;
|
||||
if (!shared.getIndexPattern(embeddable)) return false;
|
||||
if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async execute(context: Context): Promise<void> {
|
||||
if (!shared.isVisualizeEmbeddable(context.embeddable)) return;
|
||||
|
||||
const { core } = this.params.start();
|
||||
const { appName, appPath } = await this.getUrl(context);
|
||||
|
||||
await core.application.navigateToApp(appName, {
|
||||
path: appPath,
|
||||
});
|
||||
}
|
||||
|
||||
public async getHref(context: Context): Promise<string> {
|
||||
const { embeddable } = context;
|
||||
|
||||
if (!shared.isVisualizeEmbeddable(embeddable)) {
|
||||
throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`);
|
||||
}
|
||||
|
||||
const { path } = await this.getUrl(context);
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ExploreDataChartAction } from './explore_data_chart_action';
|
||||
import { Params, PluginDeps } from './abstract_explore_data_action';
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
|
||||
import {
|
||||
EmbeddableStart,
|
||||
RangeSelectTriggerContext,
|
||||
ValueClickTriggerContext,
|
||||
ChartActionContext,
|
||||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
VisualizeEmbeddableContract,
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
} from '../../../../../../src/plugins/visualizations/public';
|
||||
import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
|
||||
import { Filter, TimeRange } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance;
|
||||
|
||||
jest.mock('@kbn/i18n', () => ({
|
||||
i18n: {
|
||||
translate: jest.fn((key, options) => options.defaultMessage),
|
||||
},
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
i18nTranslateSpy.mockClear();
|
||||
});
|
||||
|
||||
const setup = ({ useRangeEvent = false }: { useRangeEvent?: boolean } = {}) => {
|
||||
type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
|
||||
|
||||
const core = coreMock.createStart();
|
||||
|
||||
const urlGenerator: UrlGenerator = ({
|
||||
createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
|
||||
} as unknown) as UrlGenerator;
|
||||
|
||||
const filtersAndTimeRangeFromContext = jest.fn((async () => ({
|
||||
filters: [],
|
||||
})) as EmbeddableStart['filtersAndTimeRangeFromContext']);
|
||||
|
||||
const plugins: PluginDeps = {
|
||||
discover: {
|
||||
urlGenerator,
|
||||
},
|
||||
embeddable: {
|
||||
filtersAndTimeRangeFromContext,
|
||||
},
|
||||
};
|
||||
|
||||
const params: Params = {
|
||||
start: () => ({
|
||||
plugins,
|
||||
self: {},
|
||||
core,
|
||||
}),
|
||||
};
|
||||
const action = new ExploreDataChartAction(params);
|
||||
|
||||
const input = {
|
||||
viewMode: ViewMode.VIEW,
|
||||
};
|
||||
|
||||
const output = {
|
||||
indexPatterns: [
|
||||
{
|
||||
id: 'index-ptr-foo',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const embeddable: VisualizeEmbeddableContract = ({
|
||||
type: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
getInput: () => input,
|
||||
getOutput: () => output,
|
||||
} as unknown) as VisualizeEmbeddableContract;
|
||||
|
||||
const data: ChartActionContext<typeof embeddable>['data'] = {
|
||||
...(useRangeEvent
|
||||
? ({ range: {} } as RangeSelectTriggerContext['data'])
|
||||
: ({ data: [] } as ValueClickTriggerContext['data'])),
|
||||
timeFieldName: 'order_date',
|
||||
};
|
||||
|
||||
const context = {
|
||||
embeddable,
|
||||
data,
|
||||
} as ChartActionContext<typeof embeddable>;
|
||||
|
||||
return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context };
|
||||
};
|
||||
|
||||
describe('"Explore underlying data" panel action', () => {
|
||||
test('action has Discover icon', () => {
|
||||
const { action, context } = setup();
|
||||
expect(action.getIconType(context)).toBe('discoverApp');
|
||||
});
|
||||
|
||||
test('title is "Explore underlying data"', () => {
|
||||
const { action, context } = setup();
|
||||
expect(action.getDisplayName(context)).toBe('Explore underlying data');
|
||||
});
|
||||
|
||||
test('translates title', () => {
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const { action, context } = setup();
|
||||
action.getDisplayName(context);
|
||||
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(i18nTranslateSpy.mock.calls[0][0]).toBe(
|
||||
'xpack.discover.FlyoutCreateDrilldownAction.displayName'
|
||||
);
|
||||
});
|
||||
|
||||
describe('isCompatible()', () => {
|
||||
test('returns true when all conditions are met', async () => {
|
||||
const { action, context } = setup();
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when URL generator is not present', async () => {
|
||||
const { action, plugins, context } = setup();
|
||||
(plugins.discover as any).urlGenerator = undefined;
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if embeddable is not Visualize embeddable', async () => {
|
||||
const { action, embeddable, context } = setup();
|
||||
(embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE';
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if embeddable does not have index patterns', async () => {
|
||||
const { action, output, context } = setup();
|
||||
delete output.indexPatterns;
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if embeddable index patterns are empty', async () => {
|
||||
const { action, output, context } = setup();
|
||||
output.indexPatterns = [];
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if dashboard is in edit mode', async () => {
|
||||
const { action, input, context } = setup();
|
||||
input.viewMode = ViewMode.EDIT;
|
||||
|
||||
const isCompatible = await action.isCompatible(context);
|
||||
|
||||
expect(isCompatible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHref()', () => {
|
||||
test('returns URL path generated by URL generator', async () => {
|
||||
const { action, context } = setup();
|
||||
|
||||
const href = await action.getHref(context);
|
||||
|
||||
expect(href).toBe('/xyz/app/discover/foo#bar');
|
||||
});
|
||||
|
||||
test('calls URL generator with right arguments', async () => {
|
||||
const { action, urlGenerator, context } = setup();
|
||||
|
||||
expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0);
|
||||
|
||||
await action.getHref(context);
|
||||
|
||||
expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1);
|
||||
expect(urlGenerator.createUrl).toHaveBeenCalledWith({
|
||||
filters: [],
|
||||
indexPatternId: 'index-ptr-foo',
|
||||
timeRange: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('applies chart event filters', async () => {
|
||||
const { action, context, urlGenerator, plugins } = setup();
|
||||
|
||||
((plugins.embeddable
|
||||
.filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => {
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
meta: {
|
||||
alias: 'alias',
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
const timeRange: TimeRange = {
|
||||
from: 'from',
|
||||
to: 'to',
|
||||
};
|
||||
return { filters, timeRange };
|
||||
});
|
||||
|
||||
expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0);
|
||||
|
||||
await action.getHref(context);
|
||||
|
||||
expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1);
|
||||
expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context);
|
||||
expect(urlGenerator.createUrl).toHaveBeenCalledWith({
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
alias: 'alias',
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
indexPatternId: 'index-ptr-foo',
|
||||
timeRange: {
|
||||
from: 'from',
|
||||
to: 'to',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute()', () => {
|
||||
test('calls platform SPA navigation method', async () => {
|
||||
const { action, context, core } = setup();
|
||||
|
||||
expect(core.application.navigateToApp).toHaveBeenCalledTimes(0);
|
||||
|
||||
await action.execute(context);
|
||||
|
||||
expect(core.application.navigateToApp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('calls platform SPA navigation method with right arguments', async () => {
|
||||
const { action, context, core } = setup();
|
||||
|
||||
await action.execute(context);
|
||||
|
||||
expect(core.application.navigateToApp).toHaveBeenCalledTimes(1);
|
||||
expect(core.application.navigateToApp.mock.calls[0]).toEqual([
|
||||
'discover',
|
||||
{
|
||||
path: '/foo#bar',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Action } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
ValueClickTriggerContext,
|
||||
RangeSelectTriggerContext,
|
||||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public';
|
||||
import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public';
|
||||
import { KibanaURL } from './kibana_url';
|
||||
import * as shared from './shared';
|
||||
import { AbstractExploreDataAction } from './abstract_explore_data_action';
|
||||
|
||||
export type ExploreDataChartActionContext = ValueClickTriggerContext | RangeSelectTriggerContext;
|
||||
|
||||
export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART';
|
||||
|
||||
/**
|
||||
* This is "Explore underlying data" action which appears in popup context
|
||||
* menu when user clicks a value in visualization or brushes a time range.
|
||||
*/
|
||||
export class ExploreDataChartAction extends AbstractExploreDataAction<ExploreDataChartActionContext>
|
||||
implements Action<ExploreDataChartActionContext> {
|
||||
public readonly id = ACTION_EXPLORE_DATA_CHART;
|
||||
|
||||
public readonly type = ACTION_EXPLORE_DATA_CHART;
|
||||
|
||||
public readonly order = 200;
|
||||
|
||||
protected readonly getUrl = async (
|
||||
context: ExploreDataChartActionContext
|
||||
): Promise<KibanaURL> => {
|
||||
const { plugins } = this.params.start();
|
||||
const { urlGenerator } = plugins.discover;
|
||||
|
||||
if (!urlGenerator) {
|
||||
throw new Error('Discover URL generator not available.');
|
||||
}
|
||||
|
||||
const { embeddable } = context;
|
||||
const { filters, timeRange } = await plugins.embeddable.filtersAndTimeRangeFromContext(context);
|
||||
const state: DiscoverUrlGeneratorState = {
|
||||
filters,
|
||||
timeRange,
|
||||
};
|
||||
|
||||
if (embeddable) {
|
||||
state.indexPatternId = shared.getIndexPattern(embeddable) || undefined;
|
||||
|
||||
const input = embeddable.getInput();
|
||||
|
||||
if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange;
|
||||
if (isQuery(input.query)) state.query = input.query;
|
||||
if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])];
|
||||
}
|
||||
|
||||
const path = await urlGenerator.createUrl(state);
|
||||
|
||||
return new KibanaURL(path);
|
||||
};
|
||||
}
|
|
@ -4,14 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExploreDataContextMenuAction,
|
||||
ACTION_EXPLORE_DATA,
|
||||
Params,
|
||||
PluginDeps,
|
||||
} from './explore_data_context_menu_action';
|
||||
import { ExploreDataContextMenuAction } from './explore_data_context_menu_action';
|
||||
import { Params, PluginDeps } from './abstract_explore_data_action';
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
|
||||
import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
VisualizeEmbeddableContract,
|
||||
|
@ -37,14 +34,20 @@ const setup = () => {
|
|||
const core = coreMock.createStart();
|
||||
|
||||
const urlGenerator: UrlGenerator = ({
|
||||
id: ACTION_EXPLORE_DATA,
|
||||
createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
|
||||
} as unknown) as UrlGenerator;
|
||||
|
||||
const filtersAndTimeRangeFromContext = jest.fn((async () => ({
|
||||
filters: [],
|
||||
})) as EmbeddableStart['filtersAndTimeRangeFromContext']);
|
||||
|
||||
const plugins: PluginDeps = {
|
||||
discover: {
|
||||
urlGenerator,
|
||||
},
|
||||
embeddable: {
|
||||
filtersAndTimeRangeFromContext,
|
||||
},
|
||||
};
|
||||
|
||||
const params: Params = {
|
||||
|
@ -83,19 +86,20 @@ const setup = () => {
|
|||
|
||||
describe('"Explore underlying data" panel action', () => {
|
||||
test('action has Discover icon', () => {
|
||||
const { action } = setup();
|
||||
expect(action.getIconType()).toBe('discoverApp');
|
||||
const { action, context } = setup();
|
||||
expect(action.getIconType(context)).toBe('discoverApp');
|
||||
});
|
||||
|
||||
test('title is "Explore underlying data"', () => {
|
||||
const { action } = setup();
|
||||
expect(action.getDisplayName()).toBe('Explore underlying data');
|
||||
const { action, context } = setup();
|
||||
expect(action.getDisplayName(context)).toBe('Explore underlying data');
|
||||
});
|
||||
|
||||
test('translates title', () => {
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
setup().action.getDisplayName();
|
||||
const { action, context } = setup();
|
||||
action.getDisplayName(context);
|
||||
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(i18nTranslateSpy.mock.calls[0][0]).toBe(
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Action } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { EmbeddableContext } from '../../../../../../src/plugins/embeddable/public';
|
||||
import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public';
|
||||
import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public';
|
||||
import { KibanaURL } from './kibana_url';
|
||||
import * as shared from './shared';
|
||||
import { AbstractExploreDataAction } from './abstract_explore_data_action';
|
||||
|
||||
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
|
||||
|
||||
/**
|
||||
* This is "Explore underlying data" action which appears in the context
|
||||
* menu of a dashboard panel.
|
||||
*/
|
||||
export class ExploreDataContextMenuAction extends AbstractExploreDataAction<EmbeddableContext>
|
||||
implements Action<EmbeddableContext> {
|
||||
public readonly id = ACTION_EXPLORE_DATA;
|
||||
|
||||
public readonly type = ACTION_EXPLORE_DATA;
|
||||
|
||||
public readonly order = 200;
|
||||
|
||||
protected readonly getUrl = async (context: EmbeddableContext): Promise<KibanaURL> => {
|
||||
const { plugins } = this.params.start();
|
||||
const { urlGenerator } = plugins.discover;
|
||||
|
||||
if (!urlGenerator) {
|
||||
throw new Error('Discover URL generator not available.');
|
||||
}
|
||||
|
||||
const { embeddable } = context;
|
||||
const state: DiscoverUrlGeneratorState = {};
|
||||
|
||||
if (embeddable) {
|
||||
state.indexPatternId = shared.getIndexPattern(embeddable) || undefined;
|
||||
|
||||
const input = embeddable.getInput();
|
||||
|
||||
if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange;
|
||||
if (isQuery(input.query)) state.query = input.query;
|
||||
if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])];
|
||||
}
|
||||
|
||||
const path = await urlGenerator.createUrl(state);
|
||||
|
||||
return new KibanaURL(path);
|
||||
};
|
||||
}
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export * from './explore_data_context_menu_action';
|
||||
export * from './explore_data_chart_action';
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// TODO: Replace this logic with KibanaURL once it is available.
|
||||
// https://github.com/elastic/kibana/issues/64497
|
||||
export class KibanaURL {
|
||||
public readonly path: string;
|
||||
public readonly appName: string;
|
||||
public readonly appPath: string;
|
||||
|
||||
constructor(path: string) {
|
||||
const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error('Unexpected Discover URL path.');
|
||||
}
|
||||
|
||||
const [, appName, appPath] = match;
|
||||
|
||||
if (!appName || !appPath) {
|
||||
throw new Error('Could not parse Discover URL path.');
|
||||
}
|
||||
|
||||
this.path = path;
|
||||
this.appName = appName;
|
||||
this.appPath = appPath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
|
||||
import {
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
VisualizeEmbeddableContract,
|
||||
} from '../../../../../../src/plugins/visualizations/public';
|
||||
|
||||
export const isOutputWithIndexPatterns = (
|
||||
output: unknown
|
||||
): output is { indexPatterns: Array<{ id: string }> } => {
|
||||
if (!output || typeof output !== 'object') return false;
|
||||
return Array.isArray((output as any).indexPatterns);
|
||||
};
|
||||
|
||||
export const isVisualizeEmbeddable = (
|
||||
embeddable?: IEmbeddable
|
||||
): embeddable is VisualizeEmbeddableContract =>
|
||||
embeddable && embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE ? true : false;
|
||||
|
||||
/**
|
||||
* @returns Returns empty string if no index pattern ID found.
|
||||
*/
|
||||
export const getIndexPattern = (embeddable?: IEmbeddable): string => {
|
||||
if (!embeddable) return '';
|
||||
const output = embeddable.getOutput();
|
||||
|
||||
if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) {
|
||||
return output.indexPatterns[0].id;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './view_in_discover';
|
||||
export * from './explore_data';
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Action } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { DiscoverStart } from '../../../../../../src/plugins/discover/public';
|
||||
import {
|
||||
EmbeddableContext,
|
||||
IEmbeddable,
|
||||
ViewMode,
|
||||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
import { CoreStart } from '../../../../../../src/core/public';
|
||||
import {
|
||||
VisualizeEmbeddableContract,
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
} from '../../../../../../src/plugins/visualizations/public';
|
||||
|
||||
// TODO: Replace this logic with KibanaURL once it is available.
|
||||
// https://github.com/elastic/kibana/issues/64497
|
||||
class KibanaURL {
|
||||
public readonly path: string;
|
||||
public readonly appName: string;
|
||||
public readonly appPath: string;
|
||||
|
||||
constructor(path: string) {
|
||||
const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error('Unexpected Discover URL path.');
|
||||
}
|
||||
|
||||
const [, appName, appPath] = match;
|
||||
|
||||
if (!appName || !appPath) {
|
||||
throw new Error('Could not parse Discover URL path.');
|
||||
}
|
||||
|
||||
this.path = path;
|
||||
this.appName = appName;
|
||||
this.appPath = appPath;
|
||||
}
|
||||
}
|
||||
|
||||
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
|
||||
|
||||
const isOutputWithIndexPatterns = (
|
||||
output: unknown
|
||||
): output is { indexPatterns: Array<{ id: string }> } => {
|
||||
if (!output || typeof output !== 'object') return false;
|
||||
return Array.isArray((output as any).indexPatterns);
|
||||
};
|
||||
|
||||
const isVisualizeEmbeddable = (
|
||||
embeddable: IEmbeddable
|
||||
): embeddable is VisualizeEmbeddableContract => embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE;
|
||||
|
||||
export interface PluginDeps {
|
||||
discover: Pick<DiscoverStart, 'urlGenerator'>;
|
||||
}
|
||||
|
||||
export interface CoreDeps {
|
||||
application: Pick<CoreStart['application'], 'navigateToApp'>;
|
||||
}
|
||||
|
||||
export interface Params {
|
||||
start: StartServicesGetter<PluginDeps, unknown, CoreDeps>;
|
||||
}
|
||||
|
||||
export class ExploreDataContextMenuAction implements Action<EmbeddableContext> {
|
||||
public readonly id = ACTION_EXPLORE_DATA;
|
||||
|
||||
public readonly type = ACTION_EXPLORE_DATA;
|
||||
|
||||
public readonly order = 200;
|
||||
|
||||
constructor(private readonly params: Params) {}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', {
|
||||
defaultMessage: 'Explore underlying data',
|
||||
});
|
||||
}
|
||||
|
||||
public getIconType() {
|
||||
return 'discoverApp';
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: EmbeddableContext) {
|
||||
if (!this.params.start().plugins.discover.urlGenerator) return false;
|
||||
if (!isVisualizeEmbeddable(embeddable)) return false;
|
||||
if (!this.getIndexPattern(embeddable)) return false;
|
||||
if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: EmbeddableContext) {
|
||||
if (!isVisualizeEmbeddable(embeddable)) return;
|
||||
|
||||
const { core } = this.params.start();
|
||||
const { appName, appPath } = await this.getUrl(embeddable);
|
||||
|
||||
await core.application.navigateToApp(appName, {
|
||||
path: appPath,
|
||||
});
|
||||
}
|
||||
|
||||
public async getHref({ embeddable }: EmbeddableContext): Promise<string> {
|
||||
if (!isVisualizeEmbeddable(embeddable)) {
|
||||
throw new Error(`Embeddable not supported for "${this.getDisplayName()}" action.`);
|
||||
}
|
||||
|
||||
const { path } = await this.getUrl(embeddable);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private async getUrl(embeddable: VisualizeEmbeddableContract): Promise<KibanaURL> {
|
||||
const { plugins } = this.params.start();
|
||||
const { urlGenerator } = plugins.discover;
|
||||
|
||||
if (!urlGenerator) {
|
||||
throw new Error('Discover URL generator not available.');
|
||||
}
|
||||
|
||||
const { timeRange, query, filters } = embeddable.getInput();
|
||||
const indexPatternId = this.getIndexPattern(embeddable);
|
||||
|
||||
const path = await urlGenerator.createUrl({
|
||||
indexPatternId,
|
||||
filters,
|
||||
query,
|
||||
timeRange,
|
||||
});
|
||||
|
||||
return new KibanaURL(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Returns empty string if no index pattern ID found.
|
||||
*/
|
||||
private getIndexPattern(embeddable: VisualizeEmbeddableContract): string {
|
||||
const output = embeddable!.getOutput();
|
||||
|
||||
if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) {
|
||||
return output.indexPatterns[0].id;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -6,7 +6,12 @@
|
|||
|
||||
import { CoreSetup, CoreStart, Plugin } from 'kibana/public';
|
||||
import { PluginInitializerContext } from 'kibana/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
UiActionsSetup,
|
||||
UiActionsStart,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '../../../../src/plugins/ui_actions/public';
|
||||
import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
|
||||
import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public';
|
||||
import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public';
|
||||
|
@ -16,11 +21,18 @@ import {
|
|||
EmbeddableContext,
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
} from '../../../../src/plugins/embeddable/public';
|
||||
import { ExploreDataContextMenuAction, ACTION_EXPLORE_DATA } from './actions';
|
||||
import {
|
||||
ExploreDataContextMenuAction,
|
||||
ExploreDataChartAction,
|
||||
ACTION_EXPLORE_DATA,
|
||||
ACTION_EXPLORE_DATA_CHART,
|
||||
ExploreDataChartActionContext,
|
||||
} from './actions';
|
||||
|
||||
declare module '../../../../src/plugins/ui_actions/public' {
|
||||
export interface ActionContextMapping {
|
||||
[ACTION_EXPLORE_DATA]: EmbeddableContext;
|
||||
[ACTION_EXPLORE_DATA_CHART]: ExploreDataChartActionContext;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,10 +60,17 @@ export class DiscoverEnhancedPlugin
|
|||
{ uiActions, share }: DiscoverEnhancedSetupDependencies
|
||||
) {
|
||||
const start = createStartServicesGetter(core.getStartServices);
|
||||
const isSharePluginInstalled = !!share;
|
||||
|
||||
if (!!share) {
|
||||
const exploreDataAction = new ExploreDataContextMenuAction({ start });
|
||||
if (isSharePluginInstalled) {
|
||||
const params = { start };
|
||||
|
||||
const exploreDataAction = new ExploreDataContextMenuAction(params);
|
||||
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, exploreDataAction);
|
||||
|
||||
const exploreDataChartAction = new ExploreDataChartAction(params);
|
||||
uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction);
|
||||
uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
);
|
||||
|
||||
// trigger drilldown action by clicking on a pie and picking drilldown action by it's name
|
||||
await pieChart.filterOnPieSlice('40,000');
|
||||
await pieChart.clickOnPieSlice('40,000');
|
||||
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
|
||||
|
||||
const href = await dashboardDrilldownPanelActions.getActionHrefByText(
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const ACTION_ID = 'ACTION_EXPLORE_DATA_CHART';
|
||||
const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`;
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const drilldowns = getService('dashboardDrilldownsManage');
|
||||
const { dashboard, discover, common, timePicker } = getPageObjects([
|
||||
'dashboard',
|
||||
'discover',
|
||||
'common',
|
||||
'timePicker',
|
||||
]);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const pieChart = getService('pieChart');
|
||||
const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions');
|
||||
const filterBar = getService('filterBar');
|
||||
const browser = getService('browser');
|
||||
|
||||
describe('Explore underlying data - chart action', () => {
|
||||
describe('value click action', () => {
|
||||
it('action exists in chart click popup menu', async () => {
|
||||
await common.navigateToApp('dashboard');
|
||||
await dashboard.preserveCrossAppState();
|
||||
await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
|
||||
await pieChart.clickOnPieSlice('160,000');
|
||||
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
|
||||
await testSubjects.existOrFail(ACTION_TEST_SUBJ);
|
||||
});
|
||||
|
||||
it('action is a link <a> element', async () => {
|
||||
const actionElement = await testSubjects.find(ACTION_TEST_SUBJ);
|
||||
const tag = await actionElement.getTagName();
|
||||
const href = await actionElement.getAttribute('href');
|
||||
|
||||
expect(tag.toLowerCase()).to.be('a');
|
||||
expect(typeof href).to.be('string');
|
||||
expect(href.length > 5).to.be(true);
|
||||
});
|
||||
|
||||
it('navigates to Discover app on action click carrying over pie slice filter', async () => {
|
||||
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
|
||||
await discover.waitForDiscoverAppOnScreen();
|
||||
await filterBar.hasFilter('memory', '160,000 to 200,000');
|
||||
const filterCount = await filterBar.getFilterCount();
|
||||
|
||||
expect(filterCount).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('brush action', () => {
|
||||
let originalTimeRangeDurationHours: number | undefined;
|
||||
|
||||
it('action exists in chart brush popup menu', async () => {
|
||||
await common.navigateToApp('dashboard');
|
||||
await dashboard.preserveCrossAppState();
|
||||
await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_AREA_CHART_NAME);
|
||||
|
||||
originalTimeRangeDurationHours = await timePicker.getTimeDurationInHours();
|
||||
const areaChart = await testSubjects.find('visualizationLoader');
|
||||
await browser.dragAndDrop(
|
||||
{
|
||||
location: areaChart,
|
||||
offset: {
|
||||
x: -100,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
location: areaChart,
|
||||
offset: {
|
||||
x: 100,
|
||||
y: 0,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
|
||||
await testSubjects.existOrFail(ACTION_TEST_SUBJ);
|
||||
});
|
||||
|
||||
it('navigates to Discover on click carrying over brushed time range', async () => {
|
||||
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
|
||||
await discover.waitForDiscoverAppOnScreen();
|
||||
const newTimeRangeDurationHours = await timePicker.getTimeDurationInHours();
|
||||
|
||||
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours as number);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -8,7 +8,7 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const ACTION_ID = 'ACTION_EXPLORE_DATA';
|
||||
const EXPLORE_RAW_DATA_ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`;
|
||||
const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`;
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const drilldowns = getService('dashboardDrilldownsManage');
|
||||
|
@ -24,31 +24,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('Explore underlying data - panel action', function () {
|
||||
before(async () => {
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' });
|
||||
before(
|
||||
'change default index pattern to verify action navigates to correct index pattern',
|
||||
async () => {
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' });
|
||||
}
|
||||
);
|
||||
|
||||
before('start on Dashboard landing page', async () => {
|
||||
await common.navigateToApp('dashboard');
|
||||
await dashboard.preserveCrossAppState();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
after('set back default index pattern', async () => {
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
|
||||
});
|
||||
|
||||
after('clean-up custom time range on panel', async () => {
|
||||
await common.navigateToApp('dashboard');
|
||||
await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
|
||||
await panelActions.openContextMenu();
|
||||
await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
|
||||
await panelActionsTimeRange.clickRemovePerPanelTimeRangeButton();
|
||||
await dashboard.saveDashboard('Dashboard with Pie Chart');
|
||||
});
|
||||
|
||||
it('action exists in panel context menu', async () => {
|
||||
await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
|
||||
await panelActions.openContextMenu();
|
||||
await testSubjects.existOrFail(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
|
||||
await testSubjects.existOrFail(ACTION_TEST_SUBJ);
|
||||
});
|
||||
|
||||
it('is a link <a> element', async () => {
|
||||
const actionElement = await testSubjects.find(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
|
||||
const actionElement = await testSubjects.find(ACTION_TEST_SUBJ);
|
||||
const tag = await actionElement.getTagName();
|
||||
|
||||
expect(tag.toLowerCase()).to.be('a');
|
||||
});
|
||||
|
||||
it('navigates to Discover app to index pattern of the panel on action click', async () => {
|
||||
await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
|
||||
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
|
||||
await discover.waitForDiscoverAppOnScreen();
|
||||
|
||||
const el = await testSubjects.find('indexPattern-switch-link');
|
||||
|
@ -71,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboard.saveDashboard('Dashboard with Pie Chart');
|
||||
|
||||
await panelActions.openContextMenu();
|
||||
await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
|
||||
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
|
||||
await discover.waitForDiscoverAppOnScreen();
|
||||
|
||||
const text = await timePicker.getShowDatesButtonText();
|
||||
|
|
|
@ -24,5 +24,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
|||
|
||||
loadTestFile(require.resolve('./dashboard_drilldowns'));
|
||||
loadTestFile(require.resolve('./explore_data_panel_action'));
|
||||
loadTestFile(require.resolve('./explore_data_chart_action'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -52,5 +52,11 @@ export function DashboardPanelTimeRangeProvider({ getService }: FtrProviderConte
|
|||
const button = await this.findModalTestSubject('addPerPanelTimeRangeButton');
|
||||
await button.click();
|
||||
}
|
||||
|
||||
public async clickRemovePerPanelTimeRangeButton() {
|
||||
log.debug('clickRemovePerPanelTimeRangeButton');
|
||||
const button = await this.findModalTestSubject('removePerPanelTimeRangeButton');
|
||||
await button.click();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue