mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Dashboard] fix custom time ranges not applied to panel until global query context changes (#155458)
Fixes https://github.com/elastic/kibana/issues/155409 https://github.com/elastic/kibana/pull/152516 incorrectly attempted to resolve https://github.com/elastic/kibana/issues/151221. https://github.com/elastic/kibana/pull/152516 updated shouldFetch$ to only check searchSessionId to determine if re-fetching is required. This logic did not work when custom time ranges are applied to panels since custom time ranges do not require new search session id yet the time range changed. This PR reverts shouldFetch$ logic of only checking searchSessionId to determine if re-fetching is required. Instead, this PR moves searchSessionId out of input and into dashboard instance state. That way, `input` updates, such as query, do not trigger additional `input` updates. The PR also updates seachSessionId logic from async to sync so that dashboard can update seachSessionId on input changes prior to child embeddables updating to parent input changes. This avoids the double fetch and allows children to only have a single input update on query state change. There was a functional test, panel_time_range, that should have caught the regression but that test had a bug in it. The assertion that the custom time range was applied looked like `expect(await testSubjects.exists('emptyPlaceholder'))` which will never fail the test because the last part of the expect is missing. Instead, the statements should be `expect(await testSubjects.exists('emptyPlaceholder')).to.be(true)`. These updates to the functional test would have caught the regression (I verified this by making these changes on main and running the test. They do indeed fail). --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: nreese <reese.nathan@elastic.co>
This commit is contained in:
parent
2376ee95e2
commit
049c51093b
13 changed files with 193 additions and 95 deletions
|
@ -7,7 +7,10 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
ContactCardEmbeddable,
|
||||
ContactCardEmbeddableFactory,
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
} from '@kbn/embeddable-plugin/public/lib/test_samples';
|
||||
import {
|
||||
|
@ -253,3 +256,63 @@ test('creates a control group from the control group factory and waits for it to
|
|||
);
|
||||
expect(mockControlGroupContainer.untilInitialized).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/*
|
||||
* dashboard.getInput$() subscriptions are used to update:
|
||||
* 1) dashboard instance searchSessionId state
|
||||
* 2) child input on parent input changes
|
||||
*
|
||||
* Rxjs subscriptions are executed in the order that they are created.
|
||||
* This test ensures that searchSessionId update subscription is created before child input subscription
|
||||
* to ensure child input subscription includes updated searchSessionId.
|
||||
*/
|
||||
test('searchSessionId is updated prior to child embeddable parent subscription execution', async () => {
|
||||
const embeddableFactory = {
|
||||
create: new ContactCardEmbeddableFactory((() => null) as any, {} as any),
|
||||
getDefaultInput: jest.fn().mockResolvedValue({
|
||||
timeRange: {
|
||||
to: 'now',
|
||||
from: 'now-15m',
|
||||
},
|
||||
}),
|
||||
};
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(embeddableFactory);
|
||||
let sessionCount = 0;
|
||||
pluginServices.getServices().data.search.session.start = () => {
|
||||
sessionCount++;
|
||||
return `searchSessionId${sessionCount}`;
|
||||
};
|
||||
const dashboard = await createDashboard(embeddableId, {
|
||||
searchSessionSettings: {
|
||||
getSearchSessionIdFromURL: () => undefined,
|
||||
removeSessionIdFromUrl: () => {},
|
||||
createSessionRestorationDataProvider: () => {},
|
||||
} as unknown as DashboardCreationOptions['searchSessionSettings'],
|
||||
});
|
||||
const embeddable = await dashboard.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
ContactCardEmbeddable
|
||||
>(CONTACT_CARD_EMBEDDABLE, {
|
||||
firstName: 'Bob',
|
||||
});
|
||||
|
||||
expect(embeddable.getInput().searchSessionId).toBe('searchSessionId1');
|
||||
|
||||
dashboard.updateInput({
|
||||
timeRange: {
|
||||
to: 'now',
|
||||
from: 'now-7d',
|
||||
},
|
||||
});
|
||||
|
||||
expect(sessionCount).toBeGreaterThan(1);
|
||||
const embeddableInput = embeddable.getInput();
|
||||
expect((embeddableInput as any).timeRange).toEqual({
|
||||
to: 'now',
|
||||
from: 'now-7d',
|
||||
});
|
||||
expect(embeddableInput.searchSessionId).toBe(`searchSessionId${sessionCount}`);
|
||||
});
|
||||
|
|
|
@ -217,6 +217,7 @@ export const createDashboard = async (
|
|||
// --------------------------------------------------------------------------------------
|
||||
// Set up search sessions integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
let initialSearchSessionId;
|
||||
if (searchSessionSettings) {
|
||||
const { sessionIdToRestore } = searchSessionSettings;
|
||||
|
||||
|
@ -229,7 +230,7 @@ export const createDashboard = async (
|
|||
}
|
||||
const existingSession = session.getSessionId();
|
||||
|
||||
const initialSearchSessionId =
|
||||
initialSearchSessionId =
|
||||
sessionIdToRestore ??
|
||||
(existingSession && incomingEmbeddable ? existingSession : session.start());
|
||||
|
||||
|
@ -238,7 +239,6 @@ export const createDashboard = async (
|
|||
creationOptions?.searchSessionSettings
|
||||
);
|
||||
});
|
||||
initialInput.searchSessionId = initialSearchSessionId;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
@ -284,6 +284,7 @@ export const createDashboard = async (
|
|||
const dashboardContainer = new DashboardContainer(
|
||||
initialInput,
|
||||
reduxEmbeddablePackage,
|
||||
initialSearchSessionId,
|
||||
savedObjectResult?.dashboardInput,
|
||||
dashboardCreationStartTime,
|
||||
undefined,
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { debounceTime, pairwise, skip } from 'rxjs/operators';
|
||||
import { pairwise, skip } from 'rxjs/operators';
|
||||
|
||||
import { noSearchSessionStorageCapabilityMessage } from '@kbn/data-plugin/public';
|
||||
|
||||
import { DashboardContainer } from '../../dashboard_container';
|
||||
import { DashboardContainerInput } from '../../../../../common';
|
||||
import { pluginServices } from '../../../../services/plugin_services';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../../../dashboard_constants';
|
||||
import { DashboardCreationOptions } from '../../dashboard_container_factory';
|
||||
import { getShouldRefresh } from '../../../state/diffing/dashboard_diffing_integration';
|
||||
|
||||
|
@ -57,10 +56,10 @@ export function startDashboardSearchSessionIntegration(
|
|||
|
||||
// listen to and compare states to determine when to launch a new session.
|
||||
this.getInput$()
|
||||
.pipe(pairwise(), debounceTime(CHANGE_CHECK_DEBOUNCE))
|
||||
.subscribe(async (states) => {
|
||||
.pipe(pairwise())
|
||||
.subscribe((states) => {
|
||||
const [previous, current] = states as DashboardContainerInput[];
|
||||
const shouldRefetch = await getShouldRefresh.bind(this)(previous, current);
|
||||
const shouldRefetch = getShouldRefresh.bind(this)(previous, current);
|
||||
if (!shouldRefetch) return;
|
||||
|
||||
const currentSearchSessionId = this.getState().explicitInput.searchSessionId;
|
||||
|
@ -83,7 +82,7 @@ export function startDashboardSearchSessionIntegration(
|
|||
})();
|
||||
|
||||
if (updatedSearchSessionId && updatedSearchSessionId !== currentSearchSessionId) {
|
||||
this.dispatch.setSearchSessionId(updatedSearchSessionId);
|
||||
this.searchSessionId = updatedSearchSessionId;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
import { findTestSubject, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import {
|
||||
|
@ -29,9 +30,10 @@ import { applicationServiceMock, coreMock } from '@kbn/core/public/mocks';
|
|||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { createEditModeActionDefinition } from '@kbn/embeddable-plugin/public/lib/test_samples';
|
||||
|
||||
import { buildMockDashboard, getSampleDashboardPanel } from '../../mocks';
|
||||
import { buildMockDashboard, getSampleDashboardInput, getSampleDashboardPanel } from '../../mocks';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { ApplicationStart } from '@kbn/core-application-browser';
|
||||
import { DashboardContainer } from './dashboard_container';
|
||||
|
||||
const theme = coreMock.createStart().theme;
|
||||
let application: ApplicationStart | undefined;
|
||||
|
@ -171,7 +173,11 @@ test('Container view mode change propagates to new children', async () => {
|
|||
|
||||
test('searchSessionId propagates to children', async () => {
|
||||
const searchSessionId1 = 'searchSessionId1';
|
||||
const container = buildMockDashboard({ searchSessionId: searchSessionId1 });
|
||||
const container = new DashboardContainer(
|
||||
getSampleDashboardInput(),
|
||||
mockedReduxEmbeddablePackage,
|
||||
searchSessionId1
|
||||
);
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
@ -181,11 +187,6 @@ test('searchSessionId propagates to children', async () => {
|
|||
});
|
||||
|
||||
expect(embeddable.getInput().searchSessionId).toBe(searchSessionId1);
|
||||
|
||||
const searchSessionId2 = 'searchSessionId2';
|
||||
container.updateInput({ searchSessionId: searchSessionId2 });
|
||||
|
||||
expect(embeddable.getInput().searchSessionId).toBe(searchSessionId2);
|
||||
});
|
||||
|
||||
test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
||||
|
|
|
@ -95,6 +95,8 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
public subscriptions: Subscription = new Subscription();
|
||||
public controlGroup?: ControlGroupContainer;
|
||||
|
||||
public searchSessionId?: string;
|
||||
|
||||
// cleanup
|
||||
public stopSyncingWithUnifiedSearch?: () => void;
|
||||
private cleanupStateTools: () => void;
|
||||
|
@ -117,6 +119,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
constructor(
|
||||
initialInput: DashboardContainerInput,
|
||||
reduxToolsPackage: ReduxToolsPackage,
|
||||
initialSessionId?: string,
|
||||
initialLastSavedInput?: DashboardContainerInput,
|
||||
dashboardCreationStartTime?: number,
|
||||
parent?: Container,
|
||||
|
@ -146,6 +149,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
} = pluginServices.getServices());
|
||||
|
||||
this.creationOptions = creationOptions;
|
||||
this.searchSessionId = initialSessionId;
|
||||
this.dashboardCreationStartTime = dashboardCreationStartTime;
|
||||
|
||||
// start diffing dashboard state
|
||||
|
@ -244,7 +248,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
syncColors,
|
||||
syncTooltips,
|
||||
hidePanelTitles,
|
||||
searchSessionId,
|
||||
refreshInterval,
|
||||
executionContext,
|
||||
} = this.input;
|
||||
|
@ -254,10 +257,10 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
combinedFilters = combineDashboardFiltersWithControlGroupFilters(filters, this.controlGroup);
|
||||
}
|
||||
return {
|
||||
searchSessionId: this.searchSessionId,
|
||||
refreshConfig: refreshInterval,
|
||||
filters: combinedFilters,
|
||||
hidePanelTitles,
|
||||
searchSessionId,
|
||||
executionContext,
|
||||
syncTooltips,
|
||||
syncColors,
|
||||
|
|
|
@ -99,13 +99,6 @@ export const dashboardContainerReducers = {
|
|||
state.explicitInput.title = action.payload;
|
||||
},
|
||||
|
||||
setSearchSessionId: (
|
||||
state: DashboardReduxState,
|
||||
action: PayloadAction<DashboardContainerInput['searchSessionId']>
|
||||
) => {
|
||||
state.explicitInput.searchSessionId = action.payload;
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Unsaved Changes Reducers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
|
|
@ -37,7 +37,7 @@ export type DashboardDiffFunctions = {
|
|||
) => boolean | Promise<boolean>;
|
||||
};
|
||||
|
||||
export const isKeyEqual = async (
|
||||
export const isKeyEqualAsync = async (
|
||||
key: keyof DashboardContainerInput,
|
||||
diffFunctionProps: DiffFunctionProps<typeof key>,
|
||||
diffingFunctions: DashboardDiffFunctions
|
||||
|
@ -52,6 +52,25 @@ export const isKeyEqual = async (
|
|||
return fastIsEqual(diffFunctionProps.currentValue, diffFunctionProps.lastValue);
|
||||
};
|
||||
|
||||
export const isKeyEqual = (
|
||||
key: keyof Omit<DashboardContainerInput, 'panels'>, // only Panels is async
|
||||
diffFunctionProps: DiffFunctionProps<typeof key>,
|
||||
diffingFunctions: DashboardDiffFunctions
|
||||
) => {
|
||||
const propsAsNever = diffFunctionProps as never; // todo figure out why props has conflicting types in some constituents.
|
||||
const diffingFunction = diffingFunctions[key];
|
||||
if (!diffingFunction) {
|
||||
return fastIsEqual(diffFunctionProps.currentValue, diffFunctionProps.lastValue);
|
||||
}
|
||||
|
||||
if (diffingFunction?.prototype?.name === 'AsyncFunction') {
|
||||
throw new Error(
|
||||
`The function for key "${key}" is async, must use isKeyEqualAsync for asynchronous functions`
|
||||
);
|
||||
}
|
||||
return diffingFunction(propsAsNever);
|
||||
};
|
||||
|
||||
/**
|
||||
* A collection of functions which diff individual keys of dashboard state. If a key is missing from this list it is
|
||||
* diffed by the default diffing function, fastIsEqual.
|
||||
|
|
|
@ -29,14 +29,14 @@ describe('getShouldRefresh', () => {
|
|||
);
|
||||
|
||||
describe('filter changes', () => {
|
||||
test('should return false when filters do not change', async () => {
|
||||
test('should return false when filters do not change', () => {
|
||||
const lastInput = {
|
||||
filters: [existsFilter],
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, lastInput)).toBe(false);
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, lastInput)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true when pinned filters change', async () => {
|
||||
test('should return true when pinned filters change', () => {
|
||||
const pinnedFilter = pinFilter(existsFilter);
|
||||
const lastInput = {
|
||||
filters: [pinnedFilter],
|
||||
|
@ -44,10 +44,10 @@ describe('getShouldRefresh', () => {
|
|||
const input = {
|
||||
filters: [toggleFilterNegated(pinnedFilter)],
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when disabled filters change', async () => {
|
||||
test('should return false when disabled filters change', () => {
|
||||
const disabledFilter = disableFilter(existsFilter);
|
||||
const lastInput = {
|
||||
filters: [disabledFilter],
|
||||
|
@ -55,29 +55,29 @@ describe('getShouldRefresh', () => {
|
|||
const input = {
|
||||
filters: [toggleFilterNegated(disabledFilter)],
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(false);
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when pinned filter changes to unpinned', async () => {
|
||||
test('should return false when pinned filter changes to unpinned', () => {
|
||||
const lastInput = {
|
||||
filters: [existsFilter],
|
||||
} as unknown as DashboardContainerInput;
|
||||
const input = {
|
||||
filters: [pinFilter(existsFilter)],
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(false);
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeRange changes', () => {
|
||||
test('should return false when timeRange does not change', async () => {
|
||||
test('should return false when timeRange does not change', () => {
|
||||
const lastInput = {
|
||||
timeRange: { from: 'now-15m', to: 'now' },
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, lastInput)).toBe(false);
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, lastInput)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true when timeRange changes (timeRestore is true)', async () => {
|
||||
test('should return true when timeRange changes (timeRestore is true)', () => {
|
||||
const lastInput = {
|
||||
timeRange: { from: 'now-15m', to: 'now' },
|
||||
timeRestore: true,
|
||||
|
@ -86,10 +86,10 @@ describe('getShouldRefresh', () => {
|
|||
timeRange: { from: 'now-30m', to: 'now' },
|
||||
timeRestore: true,
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true when timeRange changes (timeRestore is false)', async () => {
|
||||
test('should return true when timeRange changes (timeRestore is false)', () => {
|
||||
const lastInput = {
|
||||
timeRange: { from: 'now-15m', to: 'now' },
|
||||
timeRestore: false,
|
||||
|
@ -98,7 +98,26 @@ describe('getShouldRefresh', () => {
|
|||
timeRange: { from: 'now-30m', to: 'now' },
|
||||
timeRestore: false,
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('key without custom diffing function (syncColors)', () => {
|
||||
test('should return false when syncColors do not change', () => {
|
||||
const lastInput = {
|
||||
syncColors: false,
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, lastInput)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true when syncColors change', () => {
|
||||
const lastInput = {
|
||||
syncColors: false,
|
||||
} as unknown as DashboardContainerInput;
|
||||
const input = {
|
||||
syncColors: true,
|
||||
} as unknown as DashboardContainerInput;
|
||||
expect(getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,13 +9,13 @@ import { omit } from 'lodash';
|
|||
import { AnyAction, Middleware } from 'redux';
|
||||
import { debounceTime, Observable, startWith, Subject, switchMap } from 'rxjs';
|
||||
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import type { DashboardDiffFunctions } from './dashboard_diffing_functions';
|
||||
import {
|
||||
isKeyEqual,
|
||||
isKeyEqualAsync,
|
||||
shouldRefreshDiffingFunctions,
|
||||
unsavedChangesDiffingFunctions,
|
||||
} from './dashboard_diffing_functions';
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardContainer, DashboardCreationOptions } from '../..';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../../dashboard_constants';
|
||||
|
@ -29,7 +29,6 @@ import { dashboardContainerReducers } from '../dashboard_container_reducers';
|
|||
export const reducersToIgnore: Array<keyof typeof dashboardContainerReducers> = [
|
||||
'setTimeslice',
|
||||
'setFullScreenMode',
|
||||
'setSearchSessionId',
|
||||
'setExpandedPanelId',
|
||||
'setHasUnsavedChanges',
|
||||
];
|
||||
|
@ -40,7 +39,6 @@ export const reducersToIgnore: Array<keyof typeof dashboardContainerReducers> =
|
|||
const keysToOmitFromSessionStorage: Array<keyof DashboardContainerInput> = [
|
||||
'lastReloadRequestTime',
|
||||
'executionContext',
|
||||
'searchSessionId',
|
||||
'timeslice',
|
||||
'id',
|
||||
|
||||
|
@ -55,7 +53,6 @@ const keysToOmitFromSessionStorage: Array<keyof DashboardContainerInput> = [
|
|||
export const keysNotConsideredUnsavedChanges: Array<keyof DashboardContainerInput> = [
|
||||
'lastReloadRequestTime',
|
||||
'executionContext',
|
||||
'searchSessionId',
|
||||
'timeslice',
|
||||
'viewMode',
|
||||
'id',
|
||||
|
@ -64,7 +61,7 @@ export const keysNotConsideredUnsavedChanges: Array<keyof DashboardContainerInpu
|
|||
/**
|
||||
* input keys that will cause a new session to be created.
|
||||
*/
|
||||
const refetchKeys: Array<keyof DashboardContainerInput> = [
|
||||
const sessionChangeKeys: Array<keyof Omit<DashboardContainerInput, 'panels'>> = [
|
||||
'query',
|
||||
'filters',
|
||||
'timeRange',
|
||||
|
@ -139,42 +136,17 @@ export async function getUnsavedChanges(
|
|||
const allKeys = [...new Set([...Object.keys(lastInput), ...Object.keys(input)])] as Array<
|
||||
keyof DashboardContainerInput
|
||||
>;
|
||||
return await getInputChanges(this, lastInput, input, allKeys, unsavedChangesDiffingFunctions);
|
||||
}
|
||||
|
||||
export async function getShouldRefresh(
|
||||
this: DashboardContainer,
|
||||
lastInput: DashboardContainerInput,
|
||||
input: DashboardContainerInput
|
||||
): Promise<boolean> {
|
||||
const inputChanges = await getInputChanges(
|
||||
this,
|
||||
lastInput,
|
||||
input,
|
||||
refetchKeys,
|
||||
shouldRefreshDiffingFunctions
|
||||
);
|
||||
return Object.keys(inputChanges).length > 0;
|
||||
}
|
||||
|
||||
async function getInputChanges(
|
||||
container: DashboardContainer,
|
||||
lastInput: DashboardContainerInput,
|
||||
input: DashboardContainerInput,
|
||||
keys: Array<keyof DashboardContainerInput>,
|
||||
diffingFunctions: DashboardDiffFunctions
|
||||
): Promise<Partial<DashboardContainerInput>> {
|
||||
const keyComparePromises = keys.map(
|
||||
const keyComparePromises = allKeys.map(
|
||||
(key) =>
|
||||
new Promise<{ key: keyof DashboardContainerInput; isEqual: boolean }>((resolve) => {
|
||||
if (input[key] === undefined && lastInput[key] === undefined) {
|
||||
resolve({ key, isEqual: true });
|
||||
}
|
||||
|
||||
isKeyEqual(
|
||||
isKeyEqualAsync(
|
||||
key,
|
||||
{
|
||||
container,
|
||||
container: this,
|
||||
|
||||
currentValue: input[key],
|
||||
currentInput: input,
|
||||
|
@ -182,7 +154,7 @@ async function getInputChanges(
|
|||
lastValue: lastInput[key],
|
||||
lastInput,
|
||||
},
|
||||
diffingFunctions
|
||||
unsavedChangesDiffingFunctions
|
||||
).then((isEqual) => resolve({ key, isEqual }));
|
||||
})
|
||||
);
|
||||
|
@ -196,6 +168,34 @@ async function getInputChanges(
|
|||
return inputChanges;
|
||||
}
|
||||
|
||||
export function getShouldRefresh(
|
||||
this: DashboardContainer,
|
||||
lastInput: DashboardContainerInput,
|
||||
input: DashboardContainerInput
|
||||
): boolean {
|
||||
for (const key of sessionChangeKeys) {
|
||||
if (input[key] === undefined && lastInput[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!isKeyEqual(
|
||||
key,
|
||||
{
|
||||
container: this,
|
||||
currentValue: input[key],
|
||||
currentInput: input,
|
||||
lastValue: lastInput[key],
|
||||
lastInput,
|
||||
},
|
||||
shouldRefreshDiffingFunctions
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function updateUnsavedChangesState(
|
||||
this: DashboardContainer,
|
||||
unsavedChanges: Partial<DashboardContainerInput>
|
||||
|
|
|
@ -27,19 +27,24 @@ export function shouldFetch$<
|
|||
return updated$.pipe(map(() => getInput())).pipe(
|
||||
// wrapping distinctUntilChanged with startWith and skip to prime distinctUntilChanged with an initial input value.
|
||||
startWith(getInput()),
|
||||
distinctUntilChanged((a: TFilterableEmbeddableInput, b: TFilterableEmbeddableInput) => {
|
||||
// Only need to diff searchSessionId when container uses search sessions because
|
||||
// searchSessionId changes with any filter, query, or time changes
|
||||
if (a.searchSessionId !== undefined || b.searchSessionId !== undefined) {
|
||||
return a.searchSessionId === b.searchSessionId;
|
||||
}
|
||||
distinctUntilChanged(
|
||||
(previous: TFilterableEmbeddableInput, current: TFilterableEmbeddableInput) => {
|
||||
if (
|
||||
!fastIsEqual(
|
||||
[previous.searchSessionId, previous.query, previous.timeRange, previous.timeslice],
|
||||
[current.searchSessionId, current.query, current.timeRange, current.timeslice]
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fastIsEqual([a.query, a.timeRange, a.timeslice], [b.query, b.timeRange, b.timeslice])) {
|
||||
return false;
|
||||
return onlyDisabledFiltersChanged(
|
||||
previous.filters,
|
||||
current.filters,
|
||||
shouldRefreshFilterCompareOptions
|
||||
);
|
||||
}
|
||||
|
||||
return onlyDisabledFiltersChanged(a.filters, b.filters, shouldRefreshFilterCompareOptions);
|
||||
}),
|
||||
),
|
||||
skip(1)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
});
|
||||
|
||||
it('starts a session on filter change', async () => {
|
||||
await filterBar.removeAllFilters();
|
||||
await filterBar.removeFilter('animal');
|
||||
const sessionIds = await getSessionIds();
|
||||
expect(sessionIds.length).to.be(1);
|
||||
});
|
||||
|
|
|
@ -441,11 +441,6 @@ describe('embeddable', () => {
|
|||
|
||||
expect(expressionRenderer).toHaveBeenCalledTimes(1);
|
||||
|
||||
embeddable.updateInput({
|
||||
filters: [{ meta: { alias: 'test', negate: false, disabled: false } }],
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
embeddable.updateInput({
|
||||
searchSessionId: 'nextSession',
|
||||
});
|
||||
|
|
|
@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardBadgeActions.expectExistsTimeRangeBadgeAction();
|
||||
expect(await testSubjects.exists('emptyPlaceholder'));
|
||||
expect(await testSubjects.exists('emptyPlaceholder')).to.be(true);
|
||||
await PageObjects.dashboard.clickQuickSave();
|
||||
});
|
||||
|
||||
|
@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardBadgeActions.expectMissingTimeRangeBadgeAction();
|
||||
expect(await testSubjects.exists('xyVisChart'));
|
||||
expect(await testSubjects.exists('xyVisChart')).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardBadgeActions.expectExistsTimeRangeBadgeAction();
|
||||
expect(await testSubjects.exists('emptyPlaceholder'));
|
||||
expect(await testSubjects.exists('emptyPlaceholder')).to.be(true);
|
||||
await PageObjects.dashboard.clickQuickSave();
|
||||
});
|
||||
|
||||
|
@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardBadgeActions.expectMissingTimeRangeBadgeAction();
|
||||
expect(await testSubjects.exists('xyVisChart'));
|
||||
expect(await testSubjects.exists('xyVisChart')).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue