mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
17876df41a
commit
728efb319e
2 changed files with 131 additions and 20 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue