[Lens] unnecessary unsavedChanges badge on dashboard for text based (#162482)

## Summary

When you have a text based visualization in a dashboard and you click to
edit it, then the unsaved changes badge appears.

What happens is we create store, and then we run loadInitial action with
the data from the attributes and then the store gets the state from
those attributes and pushes the change to the new updater middleware
with exactly the same data. It doesn’t affect visualization in any way
as the state is correct, but the dashboard thinks there were some
changes.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marta Bondyra 2023-07-25 16:24:17 +02:00 committed by GitHub
parent 7f002de706
commit 580b1765f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 105 additions and 5 deletions

View file

@ -0,0 +1,86 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mockStoreDeps } from '../../../mocks';
import {
initEmpty,
initExisting,
makeConfigureStore,
setState,
updateDatasourceState,
updateVisualizationState,
} from '../../../state_management';
import { updatingMiddleware } from './get_edit_lens_configuration';
describe('Lens flyout', () => {
let store: ReturnType<typeof makeConfigureStore>;
const updaterFn = jest.fn();
beforeEach(() => {
store = makeConfigureStore(mockStoreDeps(), undefined, updatingMiddleware(updaterFn));
store.dispatch = jest.fn(store.dispatch);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('updatingMiddleware for the Lens flyout', () => {
test('updater is not run on store creation', () => {
expect(updaterFn).not.toHaveBeenCalled();
});
test('updater is run if modifies visualization or datasource state', () => {
store.dispatch(
updateDatasourceState({
datasourceId: 'testDatasource2',
newDatasourceState: 'newDatasourceState',
})
);
expect(updaterFn).toHaveBeenCalledWith('newDatasourceState', null);
store.dispatch(
updateVisualizationState({ visualizationId: 'testVis', newState: 'newVisState' })
);
expect(updaterFn).toHaveBeenCalledWith('newDatasourceState', 'newVisState');
});
test('updater is not run if it does not modify visualization or datasource state', () => {
// assigning the states to {} to test equality by value check
store.dispatch(
setState({
datasourceStates: {
testDatasource: { state: {}, isLoading: true },
testDatasource2: { state: {}, isLoading: true },
},
visualization: { state: {}, activeId: 'testVis' },
})
);
updaterFn.mockClear();
// testing
store.dispatch(
updateDatasourceState({
datasourceId: 'testDatasource2',
newDatasourceState: {},
})
);
store.dispatch(updateVisualizationState({ visualizationId: 'testVis', newState: {} }));
expect(updaterFn).not.toHaveBeenCalled();
});
test('updater is not run on store initialization actions', () => {
store.dispatch(
initEmpty({
newState: { visualization: { state: {}, activeId: 'testVis' } },
})
);
store.dispatch(
initExisting({
visualization: { state: {}, activeId: 'testVis' },
})
);
expect(updaterFn).not.toHaveBeenCalled();
});
});
});

View file

@ -15,7 +15,13 @@ import type { CoreStart } from '@kbn/core/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { isEqual } from 'lodash';
import type { LensPluginStartDependencies } from '../../../plugin';
import { makeConfigureStore, LensRootStore, loadInitial } from '../../../state_management';
import {
makeConfigureStore,
LensRootStore,
loadInitial,
initExisting,
initEmpty,
} from '../../../state_management';
import { generateId } from '../../../id_generator';
import type { DatasourceMap, VisualizationMap } from '../../../types';
import {
@ -38,7 +44,8 @@ function LoadingSpinnerWithOverlay() {
type UpdaterType = (datasourceState: unknown, visualizationState: unknown) => void;
const updatingMiddleware =
// exported for testing
export const updatingMiddleware =
(updater: UpdaterType) => (store: MiddlewareAPI) => (next: Dispatch) => (action: Action) => {
const {
datasourceStates: prevDatasourceStates,
@ -48,10 +55,17 @@ const updatingMiddleware =
next(action);
const { datasourceStates, visualization, activeDatasourceId } = store.getState().lens;
if (
!isEqual(prevDatasourceStates, datasourceStates) ||
!isEqual(prevVisualization, visualization) ||
prevActiveDatasourceId !== activeDatasourceId
prevActiveDatasourceId !== activeDatasourceId ||
!isEqual(
prevDatasourceStates[prevActiveDatasourceId].state,
datasourceStates[activeDatasourceId].state
) ||
!isEqual(prevVisualization, visualization)
) {
// ignore the actions that initialize the store with the state from the attributes
if (initExisting.match(action) || initEmpty.match(action)) {
return;
}
updater(datasourceStates[activeDatasourceId].state, visualization.state);
}
};