[Dashboard] Fix time range and filter state comparisons for dashboard-to-dashboard drilldowns (#124278) (#126175)

* Remove deprecated references.

* Add timeRange to DashboardState and timeRestore to DashboardContainerInput

* Ignore filters.$state during dashboard diff

* Undo remove deprecated references.

* Conditionally exclude filter state from comparison

* Inject filter.$state for context filters

* Trigger apply filters

* Add save to functional tests

* Remove unused variable

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 6f1a963d1d)

Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-02-22 14:01:59 -05:00 committed by GitHub
parent 0ec2126a0c
commit 9107a6d117
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 60 additions and 28 deletions

View file

@ -82,6 +82,7 @@ const initialInput: DashboardContainerInput = {
to: 'now',
from: 'now-1d',
},
timeRestore: false,
title: 'test',
query: {
query: '',

View file

@ -11,6 +11,7 @@ import type { KibanaExecutionContext } from 'src/core/public';
import { DashboardSavedObject } from '../../saved_dashboards';
import { getTagsFromSavedDashboard, migrateAppState } from '.';
import { EmbeddablePackageState, ViewMode } from '../../services/embeddable';
import { TimeRange } from '../../services/data';
import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters';
import {
DashboardState,
@ -74,7 +75,9 @@ export const savedObjectToDashboardState = ({
version,
usageCollection
);
if (rawState.timeRestore) {
rawState.timeRange = { from: savedDashboard.timeFrom, to: savedDashboard.timeTo } as TimeRange;
}
rawState.controlGroupInput = deserializeControlGroupFromDashboardSavedObject(
savedDashboard
) as ControlGroupInput;
@ -106,6 +109,7 @@ export const stateToDashboardContainerInput = ({
panels,
query,
title,
timeRestore,
} = dashboardState;
return {
@ -127,6 +131,7 @@ export const stateToDashboardContainerInput = ({
timeRange: {
..._.cloneDeep(timefilter.getTime()),
},
timeRestore,
executionContext,
};
};

View file

@ -15,13 +15,7 @@ import { controlGroupInputIsEqual } from './dashboard_control_group';
import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types';
import { IEmbeddable } from '../../services/embeddable';
const stateKeystoIgnore = [
'expandedPanelId',
'fullScreenMode',
'savedQuery',
'viewMode',
'tags',
] as const;
const stateKeystoIgnore = ['expandedPanelId', 'fullScreenMode', 'savedQuery', 'viewMode', 'tags'];
type DashboardStateToCompare = Omit<DashboardState, typeof stateKeystoIgnore[number]>;
const inputKeystoIgnore = ['searchSessionId', 'lastReloadRequestTime', 'executionContext'] as const;
@ -60,6 +54,9 @@ export const diffDashboardState = async ({
newState: DashboardState;
getEmbeddable: (id: string) => Promise<IEmbeddable>;
}): Promise<Partial<DashboardState>> => {
if (!newState.timeRestore) {
stateKeystoIgnore.push('timeRange');
}
const {
controlGroupInput: originalControlGroupInput,
options: originalOptions,

View file

@ -19,6 +19,7 @@ import {
setFullScreenMode,
setPanels,
setQuery,
setTimeRange,
} from '../state';
import { diffDashboardContainerInput } from './diff_dashboard_state';
import { replaceUrlHashQuery } from '../../../../kibana_utils/public';
@ -116,6 +117,10 @@ export const applyContainerChangesToState = ({
dispatchDashboardStateChange(setQuery(input.query));
}
if (input.timeRestore && !_.isEqual(input.timeRange, latestState.timeRange)) {
dispatchDashboardStateChange(setTimeRange(input.timeRange));
}
if (!_.isEqual(input.expandedPanelId, latestState.expandedPanelId)) {
dispatchDashboardStateChange(setExpandedPanelId(input.expandedPanelId));
}

View file

@ -81,6 +81,7 @@ export const syncDashboardFilterState = ({
set: ({ filters, query }) => {
intermediateFilterState.filters = cleanFiltersForSerialize(filters ?? []) || [];
intermediateFilterState.query = query || queryString.getDefaultQuery();
applyFilters(intermediateFilterState.query, intermediateFilterState.filters);
dispatchDashboardStateChange(setFiltersAndQuery(intermediateFilterState));
},
state$: $onDashboardStateChange.pipe(

View file

@ -8,7 +8,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Filter, Query } from '../../services/data';
import { Filter, Query, TimeRange } from '../../services/data';
import { ViewMode } from '../../services/embeddable';
import type { DashboardControlGroupInput } from '../lib/dashboard_control_group';
import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types';
@ -72,6 +72,9 @@ export const dashboardStateSlice = createSlice({
setTimeRestore: (state, action: PayloadAction<boolean>) => {
state.timeRestore = action.payload;
},
setTimeRange: (state, action: PayloadAction<TimeRange>) => {
state.timeRange = action.payload;
},
setDescription: (state, action: PayloadAction<string>) => {
state.description = action.payload;
},
@ -109,6 +112,7 @@ export const {
setSavedQueryId,
setDescription,
setTimeRestore,
setTimeRange,
setSyncColors,
setUseMargins,
setViewMode,

View file

@ -28,6 +28,7 @@ export function getSampleDashboardInput(
to: 'now',
from: 'now-15m',
},
timeRestore: false,
viewMode: ViewMode.VIEW,
panels: {},
...overrides,

View file

@ -66,6 +66,7 @@ export interface DashboardState {
expandedPanelId?: string;
options: DashboardOptions;
panels: DashboardPanelMap;
timeRange?: TimeRange;
controlGroupInput?: DashboardControlGroupInput;
}
@ -83,6 +84,7 @@ export interface DashboardContainerInput extends ContainerInput {
isFullScreenMode: boolean;
expandedPanelId?: string;
timeRange: TimeRange;
timeRestore: boolean;
description?: string;
useMargins: boolean;
syncColors?: boolean;

View file

@ -291,6 +291,24 @@ export class DashboardPageObject extends FtrService {
});
}
public async clearUnsavedChanges() {
this.log.debug('clearUnsavedChanges');
let switchMode = false;
if (await this.getIsInViewMode()) {
await this.switchToEditMode();
switchMode = true;
}
await this.retry.try(async () => {
// avoid flaky test by surrounding in retry
await this.testSubjects.existOrFail('dashboardUnsavedChangesBadge');
await this.clickQuickSave();
await this.testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
});
if (switchMode) {
await this.clickCancelOutOfEditMode();
}
}
public async clickNewDashboard(continueEditing = false) {
await this.listingTable.clickNewButton('createDashboardPromptButton');
if (await this.testSubjects.exists('dashboardCreateConfirm')) {

View file

@ -45,8 +45,16 @@ 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();
};
it('create dashboard to dashboard drilldown', async () => {
await PageObjects.dashboard.gotoDashboardEditMode(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME
@ -68,6 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1);
// save dashboard, navigate to view mode
await testSubjects.existOrFail('dashboardUnsavedChangesBadge');
await PageObjects.dashboard.saveDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME,
{
@ -76,6 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
exitFromEditMode: true,
}
);
await testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
});
it('use dashboard to dashboard drilldown via onClick action', async () => {
@ -85,7 +95,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('use dashboard to dashboard drilldown via getHref action', async () => {
await filterBar.removeAllFilters();
await testDashboardDrilldown(
dashboardDrilldownPanelActions.openHrefByText.bind(dashboardDrilldownPanelActions) // preserve 'this'
);
@ -130,7 +139,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
async function testDashboardDrilldown(drilldownAction: (text: string) => Promise<void>) {
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('40,000');
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
@ -151,14 +160,13 @@ 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);
});
@ -166,11 +174,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// 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();
};
});
describe('Copy to space', () => {

View file

@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const dashboardPanelActions = getService('dashboardPanelActions');
const PageObjects = getPageObjects([
@ -43,15 +42,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('panel titles - by value', () => {
const clearUnsavedChanges = async () => {
await retry.try(async () => {
// avoid flaky test by surrounding in retry
await testSubjects.existOrFail('dashboardUnsavedChangesBadge');
await PageObjects.dashboard.clickQuickSave();
await testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
});
};
it('new panel by value has empty title', async () => {
await PageObjects.lens.createAndAddLensFromDashboard({});
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
@ -60,14 +50,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('saving new panel with blank title clears "unsaved changes" badge', async () => {
await dashboardPanelActions.setCustomPanelTitle('');
await clearUnsavedChanges();
await PageObjects.dashboard.clearUnsavedChanges();
});
it('custom title causes unsaved changes and saving clears it', async () => {
await dashboardPanelActions.setCustomPanelTitle(CUSTOM_TITLE);
const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
expect(panelTitle).to.equal(CUSTOM_TITLE);
await clearUnsavedChanges();
await PageObjects.dashboard.clearUnsavedChanges();
});
it('resetting title on a by value panel sets it to the empty string', async () => {
@ -77,7 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardPanelActions.resetCustomPanelTitle();
const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
expect(panelTitle).to.equal(EMPTY_TITLE);
await clearUnsavedChanges();
await PageObjects.dashboard.clearUnsavedChanges();
});
it('blank titles are hidden in view mode', async () => {