[Dashboard] [Controls] Fix dashboard to dashboard drilldowns where source dashboard has controls (#140548) (#141306)

* Remove `isFilters` check when calculating new filters

* Remove `disabled` boolean type check as well

* Fix initial load of dashboard

* Fix pinned filters being dropped

* Fix pinned filters bug

* Try simplified logic

* Go back to modified original logic

* Add new + organize old functional tests

* Fix flakiness of new test additions

(cherry picked from commit 54e109a8e2)

Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-09-21 15:50:25 -06:00 committed by GitHub
parent dda538c079
commit 68f33229fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 223 additions and 100 deletions

View file

@ -113,11 +113,7 @@ export const unpinFilter = (filter: Filter) =>
* @public
*/
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';
!!x && typeof x === 'object' && !!(x as Filter).meta && typeof (x as Filter).meta === 'object';
/**
* @param {unknown} filters

View file

@ -10,15 +10,15 @@ import _ from 'lodash';
import type { KibanaExecutionContext } from '@kbn/core/public';
import type { ControlGroupInput } from '@kbn/controls-plugin/public';
import { type EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public';
import {
compareFilters,
isFilterPinned,
migrateFilter,
COMPARE_ALL_OPTIONS,
type Filter,
Filter,
isFilterPinned,
TimeRange,
} from '@kbn/es-query';
import { type EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { mapAndFlattenFilters } from '@kbn/data-plugin/public';
import type { DashboardSavedObject } from '../../saved_dashboards';
import { getTagsFromSavedDashboard, migrateAppState } from '.';
@ -72,6 +72,7 @@ export const savedObjectToDashboardState = ({
if (rawState.timeRestore) {
rawState.timeRange = { from: savedDashboard.timeFrom, to: savedDashboard.timeTo } as TimeRange;
}
rawState.controlGroupInput = deserializeControlGroupFromDashboardSavedObject(
savedDashboard
) as ControlGroupInput;
@ -89,9 +90,10 @@ export const stateToDashboardContainerInput = ({
executionContext,
}: StateToDashboardContainerInputProps): DashboardContainerInput => {
const {
data: { query: queryService },
data: {
query: { filterManager, timefilter: timefilterService },
},
} = pluginServices.getServices();
const { filterManager, timefilter: timefilterService } = queryService;
const { timefilter } = timefilterService;
const {
@ -109,6 +111,7 @@ export const stateToDashboardContainerInput = ({
filters: dashboardFilters,
} = dashboardState;
const migratedDashboardFilters = mapAndFlattenFilters(_.cloneDeep(dashboardFilters));
return {
refreshConfig: timefilter.getRefreshInterval(),
filters: filterManager
@ -116,8 +119,8 @@ export const stateToDashboardContainerInput = ({
.filter(
(filter) =>
isFilterPinned(filter) ||
dashboardFilters.some((dashboardFilter) =>
filtersAreEqual(migrateFilter(_.cloneDeep(dashboardFilter)), filter)
migratedDashboardFilters.some((dashboardFilter) =>
filtersAreEqual(dashboardFilter, filter)
)
),
isFullScreenMode: fullScreenMode,

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { type Filter, isFilters, isFilterPinned, Query, TimeRange } from '@kbn/es-query';
import { type Filter, isFilterPinned, Query, TimeRange } from '@kbn/es-query';
import type { KibanaLocation } from '@kbn/share-plugin/public';
import { DashboardAppLocatorParams, cleanEmptyKeys } from '@kbn/dashboard-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
@ -62,12 +62,11 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<C
if (isTimeRange(input.timeRange) && config.useCurrentDateRange)
params.timeRange = input.timeRange;
// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned)
// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned, unpinned, and from controls)
// otherwise preserve only pinned
if (isFilters(input.filters))
params.filters = config.useCurrentFilters
? input.filters
: input.filters?.filter((f) => isFilterPinned(f));
params.filters = config.useCurrentFilters
? input.filters
: input.filters?.filter((f) => isFilterPinned(f));
}
const { restOfFilters: filtersFromEvent, timeRange: timeRangeFromEvent } = extractTimeRange(

View file

@ -6,6 +6,8 @@
*/
import expect from '@kbn/expect';
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
import { FtrProviderContext } from '../../../../ftr_provider_context';
const DRILLDOWN_TO_PIE_CHART_NAME = 'Go to pie chart dashboard';
@ -18,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardDrilldownsManage = getService('dashboardDrilldownsManage');
const PageObjects = getPageObjects([
'dashboard',
'dashboardControls',
'common',
'header',
'timePicker',
@ -46,20 +49,202 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
after(async () => {
await security.testUser.restoreDefaults();
await clearFilters(dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME);
await clearFilters(dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME);
});
const clearFilters = async (dashboardName: string) => {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
await filterBar.removeAllFilters();
await PageObjects.dashboard.clearUnsavedChanges();
};
describe('test dashboard to dashboard drilldown', async () => {
before(async () => {
await createDrilldown();
});
it('create dashboard to dashboard drilldown', async () => {
after(async () => {
await cleanFiltersAndTimePicker(dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME);
await cleanFiltersAndTimePicker(dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME);
});
it('use dashboard to dashboard drilldown via onClick action', async () => {
await testCircularDashboardDrilldowns(
dashboardDrilldownPanelActions.clickActionByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
});
it('use dashboard to dashboard drilldown via getHref action', async () => {
await testCircularDashboardDrilldowns(
dashboardDrilldownPanelActions.openHrefByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
});
it('delete dashboard to dashboard drilldown', async () => {
// delete drilldown
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.openContextMenu();
await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction();
await dashboardDrilldownPanelActions.clickManageDrilldowns();
await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen();
await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]);
await dashboardDrilldownsManage.closeFlyout();
// check that drilldown notification badge is not shown
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0);
});
it('browser back/forward navigation works after drilldown navigation', async () => {
await PageObjects.dashboard.loadSavedDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
);
const originalTimeRangeDurationHours =
await PageObjects.timePicker.getTimeDurationInHours();
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});
// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
await navigateWithinDashboard(async () => {
await browser.goBack();
});
expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be(
originalTimeRangeDurationHours
);
});
const testCircularDashboardDrilldowns = async (
drilldownAction: (text: string) => Promise<void>
) => {
await testPieChartDashboardDrilldown(drilldownAction);
expect(await filterBar.getFilterCount()).to.be(1);
const originalTimeRangeDurationHours =
await PageObjects.timePicker.getTimeDurationInHours();
await PageObjects.dashboard.clearUnsavedChanges();
// 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);
await PageObjects.dashboard.clearUnsavedChanges();
};
const cleanFiltersAndTimePicker = async (dashboardName: string) => {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
await filterBar.removeAllFilters();
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.dashboard.clearUnsavedChanges();
};
});
describe('test dashboard to dashboard drilldown with controls', async () => {
before('add controls and make selections', async () => {
/** Source Dashboard */
await createDrilldown();
await addControls(dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, [
{ field: 'geo.src', type: OPTIONS_LIST_CONTROL },
{ field: 'bytes', type: RANGE_SLIDER_CONTROL },
]);
const controlIds = await PageObjects.dashboardControls.getAllControlIds();
const [optionsListControl, rangeSliderControl] = controlIds;
await PageObjects.dashboardControls.optionsListOpenPopover(optionsListControl);
await PageObjects.dashboardControls.optionsListPopoverSelectOption('CN');
await PageObjects.dashboardControls.optionsListPopoverSelectOption('US');
await PageObjects.dashboardControls.rangeSliderWaitForLoading(); // wait for range slider to respond to options list selections before proceeding
await PageObjects.dashboardControls.rangeSliderSetLowerBound(rangeSliderControl, '1000');
await PageObjects.dashboardControls.rangeSliderSetUpperBound(rangeSliderControl, '15000');
await PageObjects.dashboard.clickQuickSave();
await PageObjects.dashboard.waitForRenderComplete();
/** Destination Dashboard */
await addControls(dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, [
{ field: 'geo.src', type: OPTIONS_LIST_CONTROL },
]);
});
after(async () => {
await cleanFiltersAndControls(dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME);
await cleanFiltersAndControls(dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME);
});
it('use dashboard to dashboard drilldown via onClick action', async () => {
await testSingleDashboardDrilldown(
dashboardDrilldownPanelActions.clickActionByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
});
it('use dashboard to dashboard drilldown via getHref action', async () => {
await testSingleDashboardDrilldown(
dashboardDrilldownPanelActions.openHrefByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
});
const addControls = async (
dashboardName: string,
controls: Array<{ field: string; type: string }>
) => {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
await PageObjects.common.clearAllToasts(); // toasts get in the way of bottom "Save and close" button in create control flyout
for (const control of controls) {
await PageObjects.dashboardControls.createControl({
controlType: control.type,
dataViewTitle: 'logstash-*',
fieldName: control.field,
});
}
await PageObjects.dashboard.clickQuickSave();
};
const testSingleDashboardDrilldown = async (
drilldownAction: (text: string) => Promise<void>
) => {
await testPieChartDashboardDrilldown(drilldownAction);
// drilldown creates filter pills for control selections
expect(await filterBar.hasFilter('geo.src', 'CN, US')).to.be(true);
expect(await filterBar.hasFilter('bytes', '1,000 to 15,000')).to.be(true);
// control filter pills impact destination dashboard controls
const controlIds = await PageObjects.dashboardControls.getAllControlIds();
const optionsListControl = controlIds[0];
await PageObjects.dashboardControls.optionsListOpenPopover(optionsListControl);
expect(
await PageObjects.dashboardControls.optionsListPopoverGetAvailableOptionsCount()
).to.equal(2);
await PageObjects.dashboardControls.optionsListEnsurePopoverIsClosed(optionsListControl);
// can clear unsaved changes badge after drilldown with controls
await PageObjects.dashboard.clearUnsavedChanges();
// clean up filters in destination dashboard
await filterBar.removeAllFilters();
expect(await filterBar.getFilterCount()).to.be(0);
await PageObjects.dashboard.clickQuickSave();
};
const cleanFiltersAndControls = async (dashboardName: string) => {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
await filterBar.removeAllFilters();
await PageObjects.dashboardControls.deleteAllControls();
await PageObjects.dashboard.clickQuickSave();
};
});
const createDrilldown = async () => {
await PageObjects.dashboard.gotoDashboardEditMode(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME
);
await PageObjects.common.clearAllToasts(); // toasts get in the way of bottom "Create drilldown" button in flyout
// create drilldown
await dashboardPanelActions.openContextMenu();
await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction();
@ -87,63 +272,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
}
);
await testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
});
};
it('use dashboard to dashboard drilldown via onClick action', async () => {
await testDashboardDrilldown(
dashboardDrilldownPanelActions.clickActionByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
const testPieChartDashboardDrilldown = async (
drilldownAction: (text: string) => Promise<void>
) => {
await PageObjects.dashboard.gotoDashboardEditMode(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME
);
});
it('use dashboard to dashboard drilldown via getHref action', async () => {
await testDashboardDrilldown(
dashboardDrilldownPanelActions.openHrefByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
});
it('delete dashboard to dashboard drilldown', async () => {
// delete drilldown
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.openContextMenu();
await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction();
await dashboardDrilldownPanelActions.clickManageDrilldowns();
await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen();
await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]);
await dashboardDrilldownsManage.closeFlyout();
// check that drilldown notification badge is not shown
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0);
});
it('browser back/forward navigation works after drilldown navigation', async () => {
await PageObjects.dashboard.loadSavedDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
);
const originalTimeRangeDurationHours =
await PageObjects.timePicker.getTimeDurationInHours();
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});
// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
await navigateWithinDashboard(async () => {
await browser.goBack();
});
expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be(
originalTimeRangeDurationHours
);
});
const testDashboardDrilldown = async (drilldownAction: (text: string) => Promise<void>) => {
// trigger drilldown action by clicking on a pie and picking drilldown action by it's name
await pieChart.clickOnPieSlice('40000');
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await retry.waitFor('drilldown action menu to appear', async () => {
// avoid flakiness of context menu opening
await pieChart.clickOnPieSlice('40000'); //
return await testSubjects.exists('multipleActionsContextMenu');
});
const href = await dashboardDrilldownPanelActions.getActionHrefByText(
DRILLDOWN_TO_AREA_CHART_NAME
@ -160,25 +303,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
// check that we drilled-down with filter from pie chart
expect(await filterBar.getFilterCount()).to.be(1);
const originalTimeRangeDurationHours =
await PageObjects.timePicker.getTimeDurationInHours();
await PageObjects.dashboard.clearUnsavedChanges();
// 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);
await PageObjects.dashboard.clearUnsavedChanges();
expect(await filterBar.hasFilter('memory', '40,000 to 80,000')).to.be(true);
};
});