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:
Vadim Dalecky 2020-06-17 10:31:16 +02:00 committed by GitHub
parent effd504d0b
commit 3ee0bf2132
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 963 additions and 36 deletions

1
.github/CODEOWNERS vendored
View file

@ -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

View file

@ -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();

View file

@ -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',

View file

@ -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",

View file

@ -0,0 +1,9 @@
{
"id": "discoverEnhanced",
"version": "8.0.0",
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"requiredPlugins": ["uiActions", "embeddable", "discover"],
"optionalPlugins": ["share"]
}

View 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';

View file

@ -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',
},
]);
});
});
});

View file

@ -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 '';
}
}

View 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 './explore_data_context_menu_action';

View 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);

View 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() {}
}

View file

@ -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',
}
);
}
}

View file

@ -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',
}
);
}
}

View file

@ -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>

View file

@ -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();

View file

@ -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);
});
});
}

View file

@ -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

View file

@ -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);

View file

@ -6,3 +6,4 @@
export { DashboardDrilldownPanelActionsProvider } from './panel_drilldown_actions';
export { DashboardDrilldownsManageProvider } from './drilldowns_manage';
export { DashboardPanelTimeRangeProvider } from './panel_time_range';

View file

@ -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();
}
})();
}

View file

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