mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Explore underlying data (#68496)
* feat: 🎸 stub discover_enhanced plugin * feat: 🎸 improve view in discover action * feat: 🎸 add URL generator to "View in Discover" action * feat: 🎸 implement navigation and getHref in view raw logs actio * fix: 🐛 disable action in "edit" mode * refactor: 💡 renamce context menu view in discover action * feat: 🎸 rename action to "explore data" * fix: 🐛 correctly generate action path * feat: 🎸 add internationalization to "explore action" * fix: 🐛 correctly parse generated Discover URL path * test: 💍 setup basic functional tests * refactor: 💡 modularize url generation logic * feat: 🎸 export CommonlyUsed type * test: 💍 add test subjects to panel custom time range modal * test: 💍 add index patterna and time range functional tests * refactor: 💡 rename action file * refactor: 💡 use URL generator from Discover plugin's contract * test: 💍 add "Explore raw data" action unit tests * fix: 🐛 import share plugin to check if it is enabled * Update x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts Co-authored-by: Matthias Wilhelm <ankertal@gmail.com> * chore: 🤖 add discover_enhanced to KibanaApp codeowners * test: 💍 improve "Explore underlying data" functional tests * test: 💍 improve <a> link assertion Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>
This commit is contained in:
parent
effd504d0b
commit
3ee0bf2132
23 changed files with 963 additions and 36 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -4,6 +4,7 @@
|
|||
|
||||
# App
|
||||
/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app
|
||||
/x-pack/plugins/discover_enhanced/ @elastic/kibana-app
|
||||
/x-pack/plugins/lens/ @elastic/kibana-app
|
||||
/x-pack/plugins/graph/ @elastic/kibana-app
|
||||
/src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app
|
||||
|
|
|
@ -324,6 +324,23 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
|
|||
const nr = await el.getAttribute('data-fetch-counter');
|
||||
return Number(nr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Discover app is currently rendered on the screen.
|
||||
*/
|
||||
public async isDiscoverAppOnScreen(): Promise<boolean> {
|
||||
const result = await find.allByCssSelector('discover-app');
|
||||
return result.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until Discover app is rendered on the screen.
|
||||
*/
|
||||
public async waitForDiscoverAppOnScreen() {
|
||||
await retry.waitFor('Discover app on screen', async () => {
|
||||
return await this.isDiscoverAppOnScreen();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new DiscoverPage();
|
||||
|
|
|
@ -21,6 +21,18 @@ import moment from 'moment';
|
|||
import { FtrProviderContext } from '../ftr_provider_context.d';
|
||||
import { WebElementWrapper } from '../services/lib/web_element_wrapper';
|
||||
|
||||
export type CommonlyUsed =
|
||||
| 'Today'
|
||||
| 'This_week'
|
||||
| 'Last_15 minutes'
|
||||
| 'Last_30 minutes'
|
||||
| 'Last_1 hour'
|
||||
| 'Last_24 hours'
|
||||
| 'Last_7 days'
|
||||
| 'Last_30 days'
|
||||
| 'Last_90 days'
|
||||
| 'Last_1 year';
|
||||
|
||||
export function TimePickerProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
|
@ -30,18 +42,6 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo
|
|||
const { header, common } = getPageObjects(['header', 'common']);
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
type CommonlyUsed =
|
||||
| 'Today'
|
||||
| 'This_week'
|
||||
| 'Last_15 minutes'
|
||||
| 'Last_30 minutes'
|
||||
| 'Last_1 hour'
|
||||
| 'Last_24 hours'
|
||||
| 'Last_7 days'
|
||||
| 'Last_30 days'
|
||||
| 'Last_90 days'
|
||||
| 'Last_1 year';
|
||||
|
||||
class TimePicker {
|
||||
defaultStartTime = 'Sep 19, 2015 @ 06:31:44.000';
|
||||
defaultEndTime = 'Sep 23, 2015 @ 18:31:44.000';
|
||||
|
@ -227,6 +227,12 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo
|
|||
};
|
||||
}
|
||||
|
||||
public async getShowDatesButtonText() {
|
||||
const button = await testSubjects.find('superDatePickerShowDatesButton');
|
||||
const text = await button.getVisibleText();
|
||||
return text;
|
||||
}
|
||||
|
||||
public async getTimeDurationForSharing() {
|
||||
return await testSubjects.getAttribute(
|
||||
'dataSharedTimefilterDuration',
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"],
|
||||
"xpack.canvas": "plugins/canvas",
|
||||
"xpack.dashboard": "plugins/dashboard_enhanced",
|
||||
"xpack.discover": "plugins/discover_enhanced",
|
||||
"xpack.crossClusterReplication": "plugins/cross_cluster_replication",
|
||||
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
|
||||
"xpack.data": "plugins/data_enhanced",
|
||||
|
|
9
x-pack/plugins/discover_enhanced/kibana.json
Normal file
9
x-pack/plugins/discover_enhanced/kibana.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "discoverEnhanced",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["uiActions", "embeddable", "discover"],
|
||||
"optionalPlugins": ["share"]
|
||||
}
|
7
x-pack/plugins/discover_enhanced/public/actions/index.ts
Normal file
7
x-pack/plugins/discover_enhanced/public/actions/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './view_in_discover';
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* 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 {
|
||||
ExploreDataContextMenuAction,
|
||||
ACTION_EXPLORE_DATA,
|
||||
Params,
|
||||
PluginDeps,
|
||||
} from './explore_data_context_menu_action';
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
VisualizeEmbeddableContract,
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
} from '../../../../../../src/plugins/visualizations/public';
|
||||
import { ViewMode } from '../../../../../../src/plugins/embeddable/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 = () => {
|
||||
type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
|
||||
|
||||
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 plugins: PluginDeps = {
|
||||
discover: {
|
||||
urlGenerator,
|
||||
},
|
||||
};
|
||||
|
||||
const params: Params = {
|
||||
start: () => ({
|
||||
plugins,
|
||||
self: {},
|
||||
core,
|
||||
}),
|
||||
};
|
||||
const action = new ExploreDataContextMenuAction(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 context = {
|
||||
embeddable,
|
||||
};
|
||||
|
||||
return { core, plugins, urlGenerator, params, action, input, output, embeddable, context };
|
||||
};
|
||||
|
||||
describe('"Explore underlying data" panel action', () => {
|
||||
test('action has Discover icon', () => {
|
||||
const { action } = setup();
|
||||
expect(action.getIconType()).toBe('discoverApp');
|
||||
});
|
||||
|
||||
test('title is "Explore underlying data"', () => {
|
||||
const { action } = setup();
|
||||
expect(action.getDisplayName()).toBe('Explore underlying data');
|
||||
});
|
||||
|
||||
test('translates title', () => {
|
||||
expect(i18nTranslateSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
setup().action.getDisplayName();
|
||||
|
||||
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({
|
||||
indexPatternId: 'index-ptr-foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,156 @@
|
|||
/*
|
||||
* 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 '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './explore_data_context_menu_action';
|
17
x-pack/plugins/discover_enhanced/public/index.ts
Normal file
17
x-pack/plugins/discover_enhanced/public/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from 'kibana/public';
|
||||
import { DiscoverEnhancedPlugin } from './plugin';
|
||||
|
||||
export {
|
||||
DiscoverEnhancedPlugin,
|
||||
DiscoverEnhancedSetupDependencies,
|
||||
DiscoverEnhancedStartDependencies,
|
||||
} from './plugin';
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
new DiscoverEnhancedPlugin(initializerContext);
|
61
x-pack/plugins/discover_enhanced/public/plugin.ts
Normal file
61
x-pack/plugins/discover_enhanced/public/plugin.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart, Plugin } from 'kibana/public';
|
||||
import { PluginInitializerContext } from 'kibana/public';
|
||||
import { UiActionsSetup, UiActionsStart } 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';
|
||||
import {
|
||||
EmbeddableSetup,
|
||||
EmbeddableStart,
|
||||
EmbeddableContext,
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
} from '../../../../src/plugins/embeddable/public';
|
||||
import { ExploreDataContextMenuAction, ACTION_EXPLORE_DATA } from './actions';
|
||||
|
||||
declare module '../../../../src/plugins/ui_actions/public' {
|
||||
export interface ActionContextMapping {
|
||||
[ACTION_EXPLORE_DATA]: EmbeddableContext;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DiscoverEnhancedSetupDependencies {
|
||||
discover: DiscoverSetup;
|
||||
embeddable: EmbeddableSetup;
|
||||
share?: SharePluginSetup;
|
||||
uiActions: UiActionsSetup;
|
||||
}
|
||||
|
||||
export interface DiscoverEnhancedStartDependencies {
|
||||
discover: DiscoverStart;
|
||||
embeddable: EmbeddableStart;
|
||||
share?: SharePluginStart;
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export class DiscoverEnhancedPlugin
|
||||
implements
|
||||
Plugin<void, void, DiscoverEnhancedSetupDependencies, DiscoverEnhancedStartDependencies> {
|
||||
constructor(public readonly initializerContext: PluginInitializerContext) {}
|
||||
|
||||
setup(
|
||||
core: CoreSetup<DiscoverEnhancedStartDependencies>,
|
||||
{ uiActions, share }: DiscoverEnhancedSetupDependencies
|
||||
) {
|
||||
const start = createStartServicesGetter(core.getStartServices);
|
||||
|
||||
if (!!share) {
|
||||
const exploreDataAction = new ExploreDataContextMenuAction({ start });
|
||||
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, exploreDataAction);
|
||||
}
|
||||
}
|
||||
|
||||
start(core: CoreStart, plugins: DiscoverEnhancedStartDependencies) {}
|
||||
|
||||
stop() {}
|
||||
}
|
|
@ -103,7 +103,10 @@ export class CustomTimeRangeAction implements ActionByType<typeof CUSTOM_TIME_RA
|
|||
embeddable={embeddable}
|
||||
dateFormat={this.dateFormat}
|
||||
commonlyUsedRanges={this.commonlyUsedRanges}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
'data-test-subj': 'customizeTimeRangeModal',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,10 @@ export class CustomTimeRangeBadge implements ActionByType<typeof CUSTOM_TIME_RAN
|
|||
embeddable={embeddable}
|
||||
dateFormat={this.dateFormat}
|
||||
commonlyUsedRanges={this.commonlyUsedRanges}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
'data-test-subj': 'customizeTimeRangeModal',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ export class CustomizeTimeRangeModal extends Component<CustomizeTimeRangeProps,
|
|||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiModalBody data-test-subj="customizePanelBody">
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel',
|
||||
|
@ -126,10 +126,11 @@ export class CustomizeTimeRangeModal extends Component<CustomizeTimeRangeProps,
|
|||
};
|
||||
}
|
||||
)}
|
||||
data-test-subj="customizePanelTimeRangeDatePicker"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiModalFooter data-test-subj="customizePanelFooter">
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={true}>
|
||||
<div>
|
||||
|
|
|
@ -7,9 +7,6 @@
|
|||
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';
|
||||
|
||||
|
@ -18,8 +15,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
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');
|
||||
|
@ -30,19 +25,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
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);
|
||||
await PageObjects.dashboard.gotoDashboardEditMode(
|
||||
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME
|
||||
);
|
||||
|
||||
// create drilldown
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
|
@ -51,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen();
|
||||
await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({
|
||||
drilldownName: DRILLDOWN_TO_AREA_CHART_NAME,
|
||||
destinationDashboardTitle: DASHBOARD_WITH_AREA_CHART_NAME,
|
||||
destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME,
|
||||
});
|
||||
await dashboardDrilldownsManage.saveChanges();
|
||||
await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose();
|
||||
|
@ -60,10 +50,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
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,
|
||||
});
|
||||
await PageObjects.dashboard.saveDashboard(
|
||||
dashboardDrilldownsManage.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');
|
||||
|
@ -117,7 +110,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('browser back/forward navigation works after drilldown navigation', async () => {
|
||||
await PageObjects.dashboard.loadSavedDashboard(DASHBOARD_WITH_AREA_CHART_NAME);
|
||||
await PageObjects.dashboard.loadSavedDashboard(
|
||||
dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
|
||||
);
|
||||
const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
|
||||
await brushAreaChart();
|
||||
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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';
|
||||
const EXPLORE_RAW_DATA_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 panelActions = getService('dashboardPanelActions');
|
||||
const panelActionsTimeRange = getService('dashboardPanelTimeRange');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('Explore underlying data - panel action', function () {
|
||||
before(async () => {
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' });
|
||||
await common.navigateToApp('dashboard');
|
||||
await dashboard.preserveCrossAppState();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('is a link <a> element', async () => {
|
||||
const actionElement = await testSubjects.find(EXPLORE_RAW_DATA_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 discover.waitForDiscoverAppOnScreen();
|
||||
|
||||
const el = await testSubjects.find('indexPattern-switch-link');
|
||||
const text = await el.getVisibleText();
|
||||
|
||||
expect(text).to.be('logstash-*');
|
||||
});
|
||||
|
||||
it('carries over panel time range', async () => {
|
||||
await common.navigateToApp('dashboard');
|
||||
|
||||
await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
|
||||
|
||||
await panelActions.openContextMenu();
|
||||
await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
|
||||
await panelActionsTimeRange.clickToggleQuickMenuButton();
|
||||
await panelActionsTimeRange.clickCommonlyUsedTimeRange('Last_90 days');
|
||||
await panelActionsTimeRange.clickModalPrimaryButton();
|
||||
|
||||
await dashboard.saveDashboard('Dashboard with Pie Chart');
|
||||
|
||||
await panelActions.openContextMenu();
|
||||
await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ);
|
||||
await discover.waitForDiscoverAppOnScreen();
|
||||
|
||||
const text = await timePicker.getShowDatesButtonText();
|
||||
const lowercaseText = text.toLowerCase();
|
||||
|
||||
expect(lowercaseText.includes('last 90 days')).to.be(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -5,9 +5,24 @@
|
|||
*/
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('drilldowns', function () {
|
||||
this.tags(['skipFirefox']);
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await esArchiver.load('dashboard/drilldowns');
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('dashboard/drilldowns');
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./dashboard_drilldowns'));
|
||||
loadTestFile(require.resolve('./explore_data_panel_action'));
|
||||
});
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -20,8 +20,22 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon
|
|||
const testSubjects = getService('testSubjects');
|
||||
const flyout = getService('flyout');
|
||||
const comboBox = getService('comboBox');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
return new (class DashboardDrilldownsManage {
|
||||
readonly DASHBOARD_WITH_PIE_CHART_NAME = 'Dashboard with Pie Chart';
|
||||
readonly DASHBOARD_WITH_AREA_CHART_NAME = 'Dashboard With Area Chart';
|
||||
|
||||
async loadData() {
|
||||
log.debug('loadData');
|
||||
await esArchiver.load('dashboard/drilldowns');
|
||||
}
|
||||
|
||||
async unloadData() {
|
||||
log.debug('unloadData');
|
||||
await esArchiver.unload('dashboard/drilldowns');
|
||||
}
|
||||
|
||||
async expectsCreateDrilldownFlyoutOpen() {
|
||||
log.debug('expectsCreateDrilldownFlyoutOpen');
|
||||
await testSubjects.existOrFail(CREATE_DRILLDOWN_FLYOUT_DATA_TEST_SUBJ);
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
|
||||
export { DashboardDrilldownPanelActionsProvider } from './panel_drilldown_actions';
|
||||
export { DashboardDrilldownsManageProvider } from './drilldowns_manage';
|
||||
export { DashboardPanelTimeRangeProvider } from './panel_time_range';
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { CommonlyUsed } from '../../../../../test/functional/page_objects/time_picker';
|
||||
|
||||
export function DashboardPanelTimeRangeProvider({ getService }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
return new (class DashboardPanelTimeRange {
|
||||
public readonly MODAL_TEST_SUBJ = 'customizeTimeRangeModal';
|
||||
public readonly CUSTOM_TIME_RANGE_ACTION = 'CUSTOM_TIME_RANGE';
|
||||
|
||||
public async clickTimeRangeActionInContextMenu() {
|
||||
log.debug('clickTimeRangeActionInContextMenu');
|
||||
await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE');
|
||||
}
|
||||
|
||||
public async findModal() {
|
||||
log.debug('findModal');
|
||||
return await testSubjects.find(this.MODAL_TEST_SUBJ);
|
||||
}
|
||||
|
||||
public async findModalTestSubject(testSubject: string) {
|
||||
log.debug('findModalElement');
|
||||
const modal = await this.findModal();
|
||||
return await modal.findByCssSelector(`[data-test-subj="${testSubject}"]`);
|
||||
}
|
||||
|
||||
public async findToggleQuickMenuButton() {
|
||||
log.debug('findToggleQuickMenuButton');
|
||||
return await this.findModalTestSubject('superDatePickerToggleQuickMenuButton');
|
||||
}
|
||||
|
||||
public async clickToggleQuickMenuButton() {
|
||||
log.debug('clickToggleQuickMenuButton');
|
||||
const button = await this.findToggleQuickMenuButton();
|
||||
await button.click();
|
||||
}
|
||||
|
||||
public async clickCommonlyUsedTimeRange(time: CommonlyUsed) {
|
||||
log.debug('clickCommonlyUsedTimeRange', time);
|
||||
await testSubjects.click(`superDatePickerCommonlyUsed_${time}`);
|
||||
}
|
||||
|
||||
public async clickModalPrimaryButton() {
|
||||
log.debug('clickModalPrimaryButton');
|
||||
const button = await this.findModalTestSubject('addPerPanelTimeRangeButton');
|
||||
await button.click();
|
||||
}
|
||||
})();
|
||||
}
|
|
@ -52,6 +52,7 @@ import { TransformProvider } from './transform';
|
|||
import {
|
||||
DashboardDrilldownPanelActionsProvider,
|
||||
DashboardDrilldownsManageProvider,
|
||||
DashboardPanelTimeRangeProvider,
|
||||
} from './dashboard';
|
||||
|
||||
// define the name and providers for services that should be
|
||||
|
@ -97,4 +98,5 @@ export const services = {
|
|||
transform: TransformProvider,
|
||||
dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider,
|
||||
dashboardDrilldownsManage: DashboardDrilldownsManageProvider,
|
||||
dashboardPanelTimeRange: DashboardPanelTimeRangeProvider,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue