Skip to content

Commit

Permalink
[8.0] [Dashboard] Transfer state when drilldown is opened in a new tab (
Browse files Browse the repository at this point in the history
#124770) (#125740)

* [Dashboard] Transfer state when drilldown is opened in a new tab (#124770)

* Translate dashboard state to URL conditionally

* Add functional tests

* Fix typo in functional test descriptions

* Remove deprecated references

* Rename useUrl to be more specific

(cherry picked from commit 17a997c)

# Conflicts:
#	src/plugins/dashboard/public/index.ts

* Fix lint
  • Loading branch information
Heenawter authored Feb 16, 2022
1 parent f8566ea commit a1a5b91
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 64 deletions.
2 changes: 1 addition & 1 deletion src/plugins/dashboard/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type {
export type { DashboardUrlGeneratorState } from './url_generator';
export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator';
export type { DashboardAppLocator, DashboardAppLocatorParams } from './locator';

export { cleanEmptyKeys } from './locator';
export type { DashboardSavedObject } from './saved_dashboards';
export type { SavedDashboardPanel, DashboardContainerInput } from './types';

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/dashboard/public/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { DashboardConstants } from './dashboard_constants';
*/
const getSerializableRecord: <O>(o: O) => O & SerializableRecord = flow(JSON.stringify, JSON.parse);

const cleanEmptyKeys = (stateObj: Record<string, unknown>) => {
export const cleanEmptyKeys = (stateObj: Record<string, unknown>) => {
Object.keys(stateObj).forEach((key) => {
if (stateObj[key] === undefined) {
delete stateObj[key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ export class WebElementWrapper {
});
}

/**
* If possible, opens 'href' of this element directly through the URL
*
* @return {Promise<void>}
*/
public async openHref() {
const href = await this.getAttribute('href');
if (href) {
await this.driver.get(href);
}
}

/**
* Check if webelement wrapper has a specific class.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ export abstract class AbstractDashboardDrilldown<Context extends object = object

public abstract readonly supportedTriggers: () => string[];

protected abstract getLocation(config: Config, context: Context): Promise<KibanaLocation>;
protected abstract getLocation(
config: Config,
context: Context,
useUrlForState: boolean
): Promise<KibanaLocation>;

public readonly order = 100;

Expand All @@ -66,7 +70,7 @@ export abstract class AbstractDashboardDrilldown<Context extends object = object
};

public readonly getHref = async (config: Config, context: Context): Promise<string> => {
const { app, path } = await this.getLocation(config, context);
const { app, path } = await this.getLocation(config, context, true);
const url = await this.params.start().core.application.getUrlForApp(app, {
path,
absolute: true,
Expand All @@ -75,7 +79,7 @@ export abstract class AbstractDashboardDrilldown<Context extends object = object
};

public readonly execute = async (config: Config, context: Context) => {
const { app, path, state } = await this.getLocation(config, context);
const { app, path, state } = await this.getLocation(config, context, false);
await this.params.start().core.application.navigateToApp(app, {
path,
state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,12 @@
* 2.0.
*/

import type { Filter, RangeFilter } from '@kbn/es-query';
import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown';
import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown';
import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks';
import {
Filter,
FilterStateStore,
Query,
RangeFilter,
TimeRange,
} from '../../../../../../../src/plugins/data/common';
import {
ApplyGlobalFilterActionContext,
esFilters,
} from '../../../../../../../src/plugins/data/public';
import { FilterStateStore, Query, TimeRange } from '../../../../../../../src/plugins/data/common';
import { ApplyGlobalFilterActionContext } from '../../../../../../../src/plugins/data/public';
import {
DashboardAppLocatorDefinition,
DashboardAppLocatorParams,
Expand Down Expand Up @@ -318,7 +310,7 @@ describe('.execute() & getHref', () => {
function getFilter(isPinned: boolean, queryKey: string): Filter {
return {
$state: {
store: isPinned ? esFilters.FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE,
store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE,
},
meta: {
index: 'logstash-*',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { isFilters, isFilterPinned } from '@kbn/es-query';
import type { Filter } from '@kbn/es-query';
import type { KibanaLocation } from 'src/plugins/share/public';
import { DashboardAppLocatorParams } from '../../../../../../../src/plugins/dashboard/public';
import {
DashboardAppLocatorParams,
cleanEmptyKeys,
} from '../../../../../../../src/plugins/dashboard/public';
import { setStateToKbnUrl } from '../../../../../../../src/plugins/kibana_utils/public';
import {
ApplyGlobalFilterActionContext,
APPLY_FILTER_TRIGGER,
esFilters,
Filter,
isFilters,
isQuery,
isTimeRange,
Query,
TimeRange,
extractTimeRange,
} from '../../../../../../../src/plugins/data/public';
import { IEmbeddable, EmbeddableInput } from '../../../../../../../src/plugins/embeddable/public';
import {
Expand Down Expand Up @@ -49,7 +52,11 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<C

public readonly supportedTriggers = () => [APPLY_FILTER_TRIGGER];

protected async getLocation(config: Config, context: Context): Promise<KibanaLocation> {
protected async getLocation(
config: Config,
context: Context,
useUrlForState: boolean
): Promise<KibanaLocation> {
const params: DashboardAppLocatorParams = {
dashboardId: config.dashboardId,
};
Expand All @@ -70,11 +77,13 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<C
if (isFilters(input.filters))
params.filters = config.useCurrentFilters
? input.filters
: input.filters?.filter((f) => esFilters.isFilterPinned(f));
: input.filters?.filter((f) => isFilterPinned(f));
}

const { restOfFilters: filtersFromEvent, timeRange: timeRangeFromEvent } =
esFilters.extractTimeRange(context.filters, context.timeFieldName);
const { restOfFilters: filtersFromEvent, timeRange: timeRangeFromEvent } = extractTimeRange(
context.filters,
context.timeFieldName
);

if (filtersFromEvent) {
params.filters = [...(params.filters ?? []), ...filtersFromEvent];
Expand All @@ -85,10 +94,27 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<C
}

const location = await this.locator.getLocation(params);
if (useUrlForState) {
this.useUrlForState(location);
}

return location;
}

private useUrlForState(location: KibanaLocation<DashboardAppLocatorParams>) {
const state = location.state;
location.path = setStateToKbnUrl(
'_a',
cleanEmptyKeys({
query: state.query,
filters: state.filters?.filter((f) => !isFilterPinned(f)),
savedQuery: state.savedQuery,
}),
{ useHash: false, storeInHashQuery: true },
location.path
);
}

public readonly inject = createInject({ drilldownId: this.id });

public readonly extract = createExtract({ drilldownId: this.id });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await security.testUser.restoreDefaults();
});

it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => {
it('create dashboard to dashboard drilldown', async () => {
await PageObjects.dashboard.gotoDashboardEditMode(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME
);
Expand Down Expand Up @@ -76,47 +76,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
exitFromEditMode: true,
}
);
});

// trigger drilldown action by clicking on a pie and picking drilldown action by it's name
await pieChart.clickOnPieSlice('40,000');
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();

const href = await dashboardDrilldownPanelActions.getActionHrefByText(
DRILLDOWN_TO_AREA_CHART_NAME
it('use dashboard to dashboard drilldown via onClick action', async () => {
await testDashboardDrilldown(
dashboardDrilldownPanelActions.clickActionByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
expect(typeof href).to.be('string'); // checking that action has a href
const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href);
});

await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME);
});
// checking that href is at least pointing to the same dashboard that we are navigated to by regular click
expect(dashboardIdFromHref).to.be(
await PageObjects.dashboard.getDashboardIdFromCurrentUrl()
it('use dashboard to dashboard drilldown via getHref action', async () => {
await filterBar.removeAllFilters();
await testDashboardDrilldown(
dashboardDrilldownPanelActions.openHrefByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
});

// check that we drilled-down with filter from pie chart
expect(await filterBar.getFilterCount()).to.be(1);

const originalTimeRangeDurationHours =
await PageObjects.timePicker.getTimeDurationInHours();

// brush area chart and drilldown back to pie chat dashboard
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();

await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});

// because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied)
expect(await filterBar.getFilterCount()).to.be(1);
await pieChart.expectPieSliceCount(1);

// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);

it('delete dashboard to dashboard drilldown', async () => {
// delete drilldown
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.openContextMenu();
Expand All @@ -127,7 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]);
await dashboardDrilldownsManage.closeFlyout();

// check that drilldown notification badge is shown
// check that drilldown notification badge is not shown
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0);
});

Expand All @@ -154,6 +129,48 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
originalTimeRangeDurationHours
);
});

async function testDashboardDrilldown(drilldownAction: (text: string) => Promise<void>) {
// trigger drilldown action by clicking on a pie and picking drilldown action by it's name
await pieChart.clickOnPieSlice('40,000');
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();

const href = await dashboardDrilldownPanelActions.getActionHrefByText(
DRILLDOWN_TO_AREA_CHART_NAME
);
expect(typeof href).to.be('string'); // checking that action has a href
const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href);

await navigateWithinDashboard(async () => {
await drilldownAction(DRILLDOWN_TO_AREA_CHART_NAME);
});
// checking that href is at least pointing to the same dashboard that we are navigated to by regular click
expect(dashboardIdFromHref).to.be(
await PageObjects.dashboard.getDashboardIdFromCurrentUrl()
);

// check that we drilled-down with filter from pie chart
expect(await filterBar.getFilterCount()).to.be(1);

const originalTimeRangeDurationHours =
await PageObjects.timePicker.getTimeDurationInHours();

// brush area chart and drilldown back to pie chat dashboard
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();

await navigateWithinDashboard(async () => {
await drilldownAction(DRILLDOWN_TO_PIE_CHART_NAME);
});

// because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied)
expect(await filterBar.getFilterCount()).to.be(1);
await pieChart.expectPieSliceCount(1);

// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
}
});

describe('Copy to space', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export function DashboardDrilldownPanelActionsProvider({ getService }: FtrProvid
return item.getAttribute('href');
}

async openHrefByText(text: string) {
log.debug(`openHref: "${text}"`);
(await this.getActionWebElementByText(text)).openHref();
}

async getActionWebElementByText(text: string): Promise<WebElementWrapper> {
log.debug(`getActionWebElement: "${text}"`);
const menu = await testSubjects.find('multipleActionsContextMenu');
Expand Down

0 comments on commit a1a5b91

Please sign in to comment.