Skip to content

Commit

Permalink
[8.7] [Dashboard] fix searchSessionId not updated when pinned filter …
Browse files Browse the repository at this point in the history
…changes (#151390) (#151742)

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Dashboard] fix searchSessionId not updated when pinned filter
changes (#151390)](#151390)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Nathan
Reese","email":"[email protected]"},"sourceCommit":{"committedDate":"2023-02-21T16:31:06Z","message":"[Dashboard]
fix searchSessionId not updated when pinned filter changes
(#151390)\n\nFixes #151219
and\r\nhttps://github.com//issues/151224\r\n\r\nPR
separates shouldRefresh logic from unsavedChanges logic to
account\r\nfor difference in filter check.\r\n\r\nshouldRefresh filter
check:\r\n* includes pinned filters\r\n* excludes disabled filters\r\n*
excludes $state so pinning/unpinning a filter does not cause
a\r\nrefresh.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"cd910bee1cb062111e094c2744f153010e6b2e2c","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Dashboard","release_note:fix","Team:Presentation","loe:hours","impact:medium","auto-backport","v8.7.0","v8.8.0"],"number":151390,"url":"https://github.com/elastic/kibana/pull/151390","mergeCommit":{"message":"[Dashboard]
fix searchSessionId not updated when pinned filter changes
(#151390)\n\nFixes #151219
and\r\nhttps://github.com//issues/151224\r\n\r\nPR
separates shouldRefresh logic from unsavedChanges logic to
account\r\nfor difference in filter check.\r\n\r\nshouldRefresh filter
check:\r\n* includes pinned filters\r\n* excludes disabled filters\r\n*
excludes $state so pinning/unpinning a filter does not cause
a\r\nrefresh.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"cd910bee1cb062111e094c2744f153010e6b2e2c"}},"sourceBranch":"main","suggestedTargetBranches":["8.7"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/151390","number":151390,"mergeCommit":{"message":"[Dashboard]
fix searchSessionId not updated when pinned filter changes
(#151390)\n\nFixes #151219
and\r\nhttps://github.com//issues/151224\r\n\r\nPR
separates shouldRefresh logic from unsavedChanges logic to
account\r\nfor difference in filter check.\r\n\r\nshouldRefresh filter
check:\r\n* includes pinned filters\r\n* excludes disabled filters\r\n*
excludes $state so pinning/unpinning a filter does not cause
a\r\nrefresh.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"cd910bee1cb062111e094c2744f153010e6b2e2c"}}]}]
BACKPORT-->

Co-authored-by: Nathan Reese <[email protected]>
  • Loading branch information
kibanamachine and nreese authored Feb 21, 2023
1 parent 5aa2bdf commit 748dfcb
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 56 deletions.
10 changes: 8 additions & 2 deletions packages/kbn-es-query/src/filters/helpers/only_disabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { filter } from 'lodash';
import type { Filter } from '..';
import type { FilterCompareOptions } from './compare_filters';
import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters';

const isEnabled = (f: Filter) => f && f.meta && !f.meta.disabled;
Expand All @@ -18,10 +19,15 @@ const isEnabled = (f: Filter) => f && f.meta && !f.meta.disabled;
*
* @public
*/
export const onlyDisabledFiltersChanged = (newFilters?: Filter[], oldFilters?: Filter[]) => {
export const onlyDisabledFiltersChanged = (
newFilters?: Filter[],
oldFilters?: Filter[],
comparatorOptions?: FilterCompareOptions
) => {
// If it's the same - compare only enabled filters
const newEnabledFilters = filter(newFilters || [], isEnabled);
const oldEnabledFilters = filter(oldFilters || [], isEnabled);
const options = comparatorOptions ?? COMPARE_ALL_OPTIONS;

return compareFilters(oldEnabledFilters, newEnabledFilters, COMPARE_ALL_OPTIONS);
return compareFilters(oldEnabledFilters, newEnabledFilters, options);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
import fastIsEqual from 'fast-deep-equal';

import { persistableControlGroupInputIsEqual } from '@kbn/controls-plugin/common';
import { compareFilters, COMPARE_ALL_OPTIONS, isFilterPinned } from '@kbn/es-query';
import {
compareFilters,
COMPARE_ALL_OPTIONS,
isFilterPinned,
onlyDisabledFiltersChanged,
} from '@kbn/es-query';

import { DashboardContainer } from '../../dashboard_container';
import { DashboardContainerByValueInput } from '../../../../../common';
Expand All @@ -32,10 +37,11 @@ export type DashboardDiffFunctions = {

export const isKeyEqual = async (
key: keyof DashboardContainerByValueInput,
diffFunctionProps: DiffFunctionProps<typeof key>
diffFunctionProps: DiffFunctionProps<typeof key>,
diffingFunctions: DashboardDiffFunctions
) => {
const propsAsNever = diffFunctionProps as never; // todo figure out why props has conflicting types in some constituents.
const diffingFunction = dashboardDiffingFunctions[key];
const diffingFunction = diffingFunctions[key];
if (diffingFunction) {
return diffingFunction?.prototype?.name === 'AsyncFunction'
? await diffingFunction(propsAsNever)
Expand All @@ -48,7 +54,7 @@ export const isKeyEqual = async (
* 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.
*/
export const dashboardDiffingFunctions: DashboardDiffFunctions = {
export const unsavedChangesDiffingFunctions: DashboardDiffFunctions = {
panels: async ({ currentValue, lastValue, container }) => {
if (!getPanelLayoutsAreEqual(currentValue, lastValue)) return false;

Expand Down Expand Up @@ -81,6 +87,7 @@ export const dashboardDiffingFunctions: DashboardDiffFunctions = {
return await Promise.any(explicitInputComparePromises).catch(() => true);
},

// exclude pinned filters from comparision because pinned filters are not part of application state
filters: ({ currentValue, lastValue }) =>
compareFilters(
currentValue.filter((f) => !isFilterPinned(f)),
Expand Down Expand Up @@ -109,3 +116,15 @@ export const dashboardDiffingFunctions: DashboardDiffFunctions = {

viewMode: () => false, // When compared view mode is always considered unequal so that it gets backed up.
};

const shouldRefreshFilterCompareOptions = {
...COMPARE_ALL_OPTIONS,
// do not compare $state to avoid refreshing when filter is pinned/unpinned (which does not impact results)
state: false,
};

export const shouldRefreshDiffingFunctions: DashboardDiffFunctions = {
...unsavedChangesDiffingFunctions,
filters: ({ currentValue, lastValue }) =>
onlyDisabledFiltersChanged(lastValue, currentValue, shouldRefreshFilterCompareOptions),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { buildExistsFilter, disableFilter, pinFilter, toggleFilterNegated } from '@kbn/es-query';
import type { DataViewFieldBase, DataViewBase } from '@kbn/es-query';
import { getShouldRefresh } from './dashboard_diffing_integration';
import { DashboardContainer } from '../../dashboard_container';
import { DashboardContainerByValueInput } from '../../../../../common';

describe('getShouldRefresh', () => {
const dashboardContainerMock = {
untilInitialized: () => {},
} as unknown as DashboardContainer;

const existsFilter = buildExistsFilter(
{
name: 'myFieldName',
} as DataViewFieldBase,
{
id: 'myDataViewId',
} as DataViewBase
);

test('should return true when pinned filters change', async () => {
const pinnedFilter = pinFilter(existsFilter);
const lastInput = {
filters: [pinnedFilter],
} as unknown as DashboardContainerByValueInput;
const input = {
filters: [toggleFilterNegated(pinnedFilter)],
} as unknown as DashboardContainerByValueInput;
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, { ...lastInput })).toBe(
false
);
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(true);
});

test('should return false when disabled filters change', async () => {
const disabledFilter = disableFilter(existsFilter);
const lastInput = {
filters: [disabledFilter],
} as unknown as DashboardContainerByValueInput;
const input = {
filters: [toggleFilterNegated(disabledFilter)],
} as unknown as DashboardContainerByValueInput;
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(false);
});

test('should return false when pinned filter changes to unpinned', async () => {
const lastInput = {
filters: [existsFilter],
} as unknown as DashboardContainerByValueInput;
const input = {
filters: [pinFilter(existsFilter)],
} as unknown as DashboardContainerByValueInput;
expect(await getShouldRefresh.bind(dashboardContainerMock)(lastInput, input)).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import { DashboardContainer } from '../../dashboard_container';
import { pluginServices } from '../../../../services/plugin_services';
import { DashboardContainerByValueInput } from '../../../../../common';
import { CHANGE_CHECK_DEBOUNCE } from '../../../../dashboard_constants';
import { isKeyEqual } from './dashboard_diffing_functions';
import type { DashboardDiffFunctions } from './dashboard_diffing_functions';
import {
isKeyEqual,
shouldRefreshDiffingFunctions,
unsavedChangesDiffingFunctions,
} from './dashboard_diffing_functions';
import { dashboardContainerReducers } from '../../../state/dashboard_container_reducers';

/**
Expand Down Expand Up @@ -54,6 +59,23 @@ export const keysNotConsideredUnsavedChanges: Array<keyof DashboardContainerByVa
'viewMode',
];

/**
* input keys that will cause a new session to be created.
*/
const refetchKeys: Array<keyof DashboardContainerByValueInput> = [
'query',
'filters',
'timeRange',
'timeslice',
'timeRestore',
'lastReloadRequestTime',

// also refetch when chart settings change
'syncColors',
'syncCursor',
'syncTooltips',
];

/**
* Does an initial diff between @param initialInput and @param initialLastSavedInput, and created a middleware
* which listens to the redux store and checks for & publishes the unsaved changes on dispatches.
Expand Down Expand Up @@ -139,44 +161,67 @@ function backupUnsavedChanges(
}

/**
* Does a shallow diff between @param lastExplicitInput and @param currentExplicitInput and
* Does a shallow diff between @param lastInput and @param input and
* @returns an object out of the keys which are different.
*/
export async function getUnsavedChanges(
this: DashboardContainer,
lastInput: DashboardContainerByValueInput,
input: DashboardContainerByValueInput
): Promise<Partial<DashboardContainerByValueInput>> {
const allKeys = [...new Set([...Object.keys(lastInput), ...Object.keys(input)])] as Array<
keyof DashboardContainerByValueInput
>;
return await getInputChanges(this, lastInput, input, allKeys, unsavedChangesDiffingFunctions);
}

export async function getShouldRefresh(
this: DashboardContainer,
lastInput: DashboardContainerByValueInput,
input: DashboardContainerByValueInput
): Promise<boolean> {
const inputChanges = await getInputChanges(
this,
lastInput,
input,
refetchKeys,
shouldRefreshDiffingFunctions
);
return Object.keys(inputChanges).length > 0;
}

async function getInputChanges(
container: DashboardContainer,
lastInput: DashboardContainerByValueInput,
input: DashboardContainerByValueInput,
keys?: Array<keyof DashboardContainerByValueInput>
keys: Array<keyof DashboardContainerByValueInput>,
diffingFunctions: DashboardDiffFunctions
): Promise<Partial<DashboardContainerByValueInput>> {
await this.untilInitialized();
const allKeys =
keys ??
([...new Set([...Object.keys(lastInput), ...Object.keys(input)])] as Array<
keyof DashboardContainerByValueInput
>);
const keyComparePromises = allKeys.map(
await container.untilInitialized();
const keyComparePromises = keys.map(
(key) =>
new Promise<{ key: keyof DashboardContainerByValueInput; isEqual: boolean }>((resolve) =>
isKeyEqual(key, {
container: this,

currentValue: input[key],
currentInput: input,

lastValue: lastInput[key],
lastInput,
}).then((isEqual) => resolve({ key, isEqual }))
isKeyEqual(
key,
{
container,

currentValue: input[key],
currentInput: input,

lastValue: lastInput[key],
lastInput,
},
diffingFunctions
).then((isEqual) => resolve({ key, isEqual }))
)
);
const unsavedChanges = (await Promise.allSettled(keyComparePromises)).reduce(
(changes, current) => {
if (current.status === 'fulfilled') {
const { key, isEqual } = current.value;
if (!isEqual) (changes as { [key: string]: unknown })[key] = input[key];
}
return changes;
},
{} as Partial<DashboardContainerByValueInput>
);
return unsavedChanges;
const inputChanges = (await Promise.allSettled(keyComparePromises)).reduce((changes, current) => {
if (current.status === 'fulfilled') {
const { key, isEqual } = current.value;
if (!isEqual) (changes as { [key: string]: unknown })[key] = input[key];
}
return changes;
}, {} as Partial<DashboardContainerByValueInput>);
return inputChanges;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,7 @@ import { pluginServices } from '../../../../services/plugin_services';
import { DashboardContainerByValueInput } from '../../../../../common';
import { CHANGE_CHECK_DEBOUNCE } from '../../../../dashboard_constants';
import { DashboardCreationOptions } from '../../dashboard_container_factory';
import { getUnsavedChanges } from '../diff_state/dashboard_diffing_integration';

/**
* input keys that will cause a new session to be created.
*/
const refetchKeys: Array<keyof DashboardContainerByValueInput> = [
'query',
'filters',
'timeRange',
'timeslice',
'timeRestore',
'lastReloadRequestTime',

// also refetch when chart settings change
'syncColors',
'syncCursor',
'syncTooltips',
];
import { getShouldRefresh } from '../diff_state/dashboard_diffing_integration';

/**
* Enables dashboard search sessions.
Expand Down Expand Up @@ -96,8 +79,7 @@ export function startDashboardSearchSessionIntegration(
.pipe(pairwise(), debounceTime(CHANGE_CHECK_DEBOUNCE))
.subscribe(async (states) => {
const [previous, current] = states as DashboardContainerByValueInput[];
const changes = await getUnsavedChanges.bind(this)(previous, current, refetchKeys);
const shouldRefetch = Object.keys(changes).length > 0;
const shouldRefetch = await getShouldRefresh.bind(this)(previous, current);
if (!shouldRefetch) return;

const {
Expand Down

0 comments on commit 748dfcb

Please sign in to comment.