[Lens] [Embeddable] Fix Lens embeddable double refetch when attributes/savedObjectId and search context change (#153517)

## Summary

This PR fixes an issue where the Lens embeddable triggers a double fetch
when both attributes/savedObjectId and search context are changed at the
same time.

Fixes #153561.

### Checklist

- [ ] ~Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)~
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~
- [ ] ~[Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios~
- [ ] ~Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard
accessibility](https://webaim.org/techniques/keyboard/))~
- [ ] ~Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~
- [ ] ~If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~
- [ ] ~This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))~
- [ ] ~This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)~

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Davis McPhee 2023-03-24 10:35:30 -03:00 committed by GitHub
parent 17876df41a
commit 728efb319e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 131 additions and 20 deletions

View file

@ -1574,4 +1574,107 @@ describe('embeddable', () => {
expect(expressionRenderer).toHaveBeenCalledTimes(4);
expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined);
});
it('should reload only once when the attributes or savedObjectId and the search context change at the same time', async () => {
const createEmbeddable = async () => {
const currentExpressionRenderer = jest.fn((_props) => null);
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }];
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer: currentExpressionRenderer,
coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
capabilities: {
canSaveDashboards: true,
canSaveVisualizations: true,
discover: {},
navLinks: {},
},
getTrigger,
visualizationMap: defaultVisualizationMap,
datasourceMap: defaultDatasourceMap,
injectFilterReferences: jest.fn(mockInjectFilterReferences),
theme: themeServiceMock.createStartContract(),
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
indexPatterns: {},
indexPatternRefs: [],
}),
},
{ id: '123', timeRange, query, filters } as LensEmbeddableInput
);
const reload = jest.spyOn(embeddable, 'reload');
const initializeSavedVis = jest.spyOn(embeddable, 'initializeSavedVis');
await embeddable.initializeSavedVis({
id: '123',
timeRange,
query,
filters,
} as LensEmbeddableInput);
embeddable.render(mountpoint);
return {
embeddable,
reload,
initializeSavedVis,
expressionRenderer: currentExpressionRenderer,
};
};
let test = await createEmbeddable();
expect(test.reload).toHaveBeenCalledTimes(1);
expect(test.initializeSavedVis).toHaveBeenCalledTimes(1);
expect(test.expressionRenderer).toHaveBeenCalledTimes(1);
// Test with savedObjectId and searchSessionId change
act(() => {
test.embeddable.updateInput({ savedObjectId: '123', searchSessionId: '456' });
});
// wait one tick to give embeddable time to initialize
await new Promise((resolve) => setTimeout(resolve, 0));
expect(test.reload).toHaveBeenCalledTimes(2);
expect(test.initializeSavedVis).toHaveBeenCalledTimes(2);
expect(test.expressionRenderer).toHaveBeenCalledTimes(2);
test = await createEmbeddable();
expect(test.reload).toHaveBeenCalledTimes(1);
expect(test.initializeSavedVis).toHaveBeenCalledTimes(1);
expect(test.expressionRenderer).toHaveBeenCalledTimes(1);
// Test with attributes and timeRange change
act(() => {
test.embeddable.updateInput({
attributes: { foo: 'bar' } as unknown as LensSavedObjectAttributes,
timeRange: { from: 'now-30d', to: 'now' },
});
});
// wait one tick to give embeddable time to initialize
await new Promise((resolve) => setTimeout(resolve, 0));
expect(test.reload).toHaveBeenCalledTimes(2);
expect(test.initializeSavedVis).toHaveBeenCalledTimes(2);
expect(test.expressionRenderer).toHaveBeenCalledTimes(2);
});
});

View file

@ -30,10 +30,10 @@ import {
} from '@kbn/data-plugin/public';
import type { Start as InspectorStart } from '@kbn/inspector-plugin/public';
import { Subscription } from 'rxjs';
import { merge, Subscription } from 'rxjs';
import { toExpression, Ast } from '@kbn/interpreter';
import { DefaultInspectorAdapters, ErrorLike, RenderMode } from '@kbn/expressions-plugin/common';
import { map, distinctUntilChanged, skip } from 'rxjs/operators';
import { map, distinctUntilChanged, skip, debounceTime } from 'rxjs/operators';
import fastIsEqual from 'fast-deep-equal';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
@ -461,34 +461,42 @@ export class Embeddable
})
);
// Use a trigger to distinguish between observables in the subscription
const withTrigger = (trigger: 'attributesOrSavedObjectId' | 'searchContext') =>
map((input: LensEmbeddableInput) => ({ trigger, input }));
// Re-initialize the visualization if either the attributes or the saved object id changes
this.inputReloadSubscriptions.push(
input$
.pipe(
distinctUntilChanged((a, b) =>
fastIsEqual(
['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId],
['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId]
)
),
skip(1)
const attributesOrSavedObjectId$ = input$.pipe(
distinctUntilChanged((a, b) =>
fastIsEqual(
['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId],
['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId]
)
.subscribe(async (input) => {
await this.initializeSavedVis(input);
this.reload();
})
),
skip(1),
withTrigger('attributesOrSavedObjectId')
);
// Update search context and reload on changes related to search
const searchContext$ = shouldFetch$<LensEmbeddableInput>(input$, () => this.getInput()).pipe(
withTrigger('searchContext')
);
// Merge and debounce the observables to avoid multiple reloads
this.inputReloadSubscriptions.push(
shouldFetch$<LensEmbeddableInput>(this.getUpdated$(), () => this.getInput()).subscribe(
(input) => {
merge(searchContext$, attributesOrSavedObjectId$)
.pipe(debounceTime(0))
.subscribe(async ({ trigger, input }) => {
if (trigger === 'attributesOrSavedObjectId') {
await this.initializeSavedVis(input);
}
// reset removable messages
// Dashboard search/context changes are detected here
this.additionalUserMessages = {};
this.reload();
}
)
})
);
}