mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Dashboard] [Controls] Fix dashboard to dashboard drilldowns where source dashboard has controls (#140548)
* 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
This commit is contained in:
parent
eae478dbe9
commit
54e109a8e2
4 changed files with 223 additions and 100 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue