From 87365ff2b7e85b3ab70cf94405536487b2ea8e2f Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 24 Nov 2025 23:00:19 +0000 Subject: [PATCH 1/3] Support by-value explore embeddables without stored saved object Signed-off-by: Joshua Li --- .../fetch/get_search_params.test.ts | 42 +++++- .../search_source/fetch/get_search_params.ts | 2 +- .../search_source/search_source.test.ts | 82 +++++++++++ .../search/search_source/search_source.ts | 2 +- .../embeddable/explore_embeddable.test.tsx | 134 ++++++++++++++---- .../public/embeddable/explore_embeddable.tsx | 9 +- .../explore_embeddable_factory.test.tsx | 131 ++++++++++++++++- .../embeddable/explore_embeddable_factory.tsx | 65 ++++++++- .../explore/public/embeddable/types.ts | 18 ++- 9 files changed, 446 insertions(+), 39 deletions(-) diff --git a/src/plugins/data/common/search/search_source/fetch/get_search_params.test.ts b/src/plugins/data/common/search/search_source/fetch/get_search_params.test.ts index 21e3bc39ce68..81dbda33f733 100644 --- a/src/plugins/data/common/search/search_source/fetch/get_search_params.test.ts +++ b/src/plugins/data/common/search/search_source/fetch/get_search_params.test.ts @@ -30,7 +30,7 @@ import { UI_SETTINGS } from '../../../constants'; import { GetConfigFn } from '../../../types'; -import { getSearchParams } from './get_search_params'; +import { getSearchParams, getExternalSearchParamsFromRequest } from './get_search_params'; function getConfigStub(config: any = {}): GetConfigFn { return (key) => config[key]; @@ -46,3 +46,43 @@ describe('getSearchParams', () => { expect(searchParams.preference).toBe('aaa'); }); }); + +describe('getExternalSearchParamsFromRequest', () => { + const getConfig = getConfigStub({}); + + test('handles index with title property', () => { + const searchRequest = { + index: { title: 'my-index' }, + body: { query: {} }, + }; + const result = getExternalSearchParamsFromRequest(searchRequest as any, { getConfig }); + expect(result.index).toBe('my-index'); + }); + + test('handles index as string', () => { + const searchRequest = { + index: 'my-index-string', + body: { query: {} }, + }; + const result = getExternalSearchParamsFromRequest(searchRequest as any, { getConfig }); + expect(result.index).toBe('my-index-string'); + }); + + test('handles undefined index with optional chaining', () => { + const searchRequest = { + index: undefined, + body: { query: {} }, + }; + const result = getExternalSearchParamsFromRequest(searchRequest as any, { getConfig }); + expect(result.index).toBeUndefined(); + }); + + test('handles null index with optional chaining', () => { + const searchRequest = { + index: null, + body: { query: {} }, + }; + const result = getExternalSearchParamsFromRequest(searchRequest as any, { getConfig }); + expect(result.index).toBeNull(); + }); +}); diff --git a/src/plugins/data/common/search/search_source/fetch/get_search_params.ts b/src/plugins/data/common/search/search_source/fetch/get_search_params.ts index d4beab6e8c71..cf60aba82198 100644 --- a/src/plugins/data/common/search/search_source/fetch/get_search_params.ts +++ b/src/plugins/data/common/search/search_source/fetch/get_search_params.ts @@ -57,7 +57,7 @@ export function getExternalSearchParamsFromRequest( ): ISearchRequestParams { const { getConfig } = dependencies; const searchParams = getSearchParams(getConfig); - const indexTitle = searchRequest.index.title || searchRequest.index; + const indexTitle = searchRequest.index?.title || searchRequest.index; return { index: indexTitle, diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 09adc867d213..f18135297446 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -236,6 +236,88 @@ describe('SearchSource', () => { }); }); + describe('#createDataFrame()', () => { + test('handles index with title property', async () => { + let storedDataFrame: any; + const mockDf = { + get: jest.fn(() => storedDataFrame), + set: jest.fn((df) => { + storedDataFrame = df; + }), + clear: jest.fn(), + }; + const deps = { ...searchSourceDependencies, df: mockDf }; + const searchSource = new SearchSource({}, deps); + const searchRequest = { + index: { title: 'my-index' }, + body: {}, + }; + const result = await searchSource.createDataFrame(searchRequest as any); + expect(mockDf.set).toHaveBeenCalled(); + expect(storedDataFrame?.name).toBe('my-index'); + }); + + test('handles index as string', async () => { + let storedDataFrame: any; + const mockDf = { + get: jest.fn(() => storedDataFrame), + set: jest.fn((df) => { + storedDataFrame = df; + }), + clear: jest.fn(), + }; + const deps = { ...searchSourceDependencies, df: mockDf }; + const searchSource = new SearchSource({}, deps); + const searchRequest = { + index: 'my-index-string', + body: {}, + }; + const result = await searchSource.createDataFrame(searchRequest as any); + expect(mockDf.set).toHaveBeenCalled(); + expect(storedDataFrame?.name).toBe('my-index-string'); + }); + + test('handles undefined index with optional chaining', async () => { + let storedDataFrame: any; + const mockDf = { + get: jest.fn(() => storedDataFrame), + set: jest.fn((df) => { + storedDataFrame = df; + }), + clear: jest.fn(), + }; + const deps = { ...searchSourceDependencies, df: mockDf }; + const searchSource = new SearchSource({}, deps); + const searchRequest = { + index: undefined, + body: {}, + }; + const result = await searchSource.createDataFrame(searchRequest as any); + expect(mockDf.set).toHaveBeenCalled(); + expect(storedDataFrame?.name).toBeUndefined(); + }); + + test('handles null index with optional chaining', async () => { + let storedDataFrame: any; + const mockDf = { + get: jest.fn(() => storedDataFrame), + set: jest.fn((df) => { + storedDataFrame = df; + }), + clear: jest.fn(), + }; + const deps = { ...searchSourceDependencies, df: mockDf }; + const searchSource = new SearchSource({}, deps); + const searchRequest = { + index: null, + body: {}, + }; + const result = await searchSource.createDataFrame(searchRequest as any); + expect(mockDf.set).toHaveBeenCalled(); + expect(storedDataFrame?.name).toBeNull(); + }); + }); + describe('#serialize', () => { test('should reference index patterns', () => { const indexPattern123 = { id: '123' } as IndexPattern; diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 787cb8700933..9c0a4aa977a0 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -327,7 +327,7 @@ export class SearchSource { */ async createDataFrame(searchRequest: SearchRequest) { const dataFrame = createDataFrame({ - name: searchRequest.index.title || searchRequest.index, + name: searchRequest.index?.title || searchRequest.index, fields: [], }); await this.setDataFrame(dataFrame); diff --git a/src/plugins/explore/public/embeddable/explore_embeddable.test.tsx b/src/plugins/explore/public/embeddable/explore_embeddable.test.tsx index f133aeec1280..24a5c9c70fce 100644 --- a/src/plugins/explore/public/embeddable/explore_embeddable.test.tsx +++ b/src/plugins/explore/public/embeddable/explore_embeddable.test.tsx @@ -358,34 +358,128 @@ describe('ExploreEmbeddable', () => { expect(mockExecuteTriggerActions).toHaveBeenCalled(); }); - test('throws error when render is called without search props', () => { - // Create a new embeddable without search props + test('onFilter returns early when indexPattern is not available', async () => { + const mockSavedExploreNoIndex = { + ...mockSavedExplore, + searchSource: { + ...mockSavedExplore.searchSource, + getField: jest.fn().mockImplementation((field) => { + if (field === 'index') return null; + if (field === 'query') return { query: 'test', language: 'PPL' }; + return null; + }), + }, + }; + + const mockServices = discoverPluginMock.createExploreServicesMock(); + mockServices.data.query.queryString.getLanguageService = jest.fn().mockReturnValue({ + getLanguage: jest.fn().mockReturnValue({ + fields: { + formatter: jest.fn(), + }, + }), + }); + mockServices.uiSettings.get = jest.fn().mockImplementation((key) => { + if (key === 'doc_table:hideTimeColumn') return false; + return 500; + }); + + const mockExecuteTriggerActionsLocal = jest.fn(); + const embeddableNoIndex = new ExploreEmbeddable( + { + savedExplore: mockSavedExploreNoIndex, + editUrl: '/app/explore/logs/test', + editPath: 'test', + indexPatterns: [], + editable: true, + filterManager: mockServices.filterManager, + services: mockServices, + editApp: 'explore/logs', + }, + mockInput, + mockExecuteTriggerActionsLocal + ); + + // Manually set searchProps to enable onFilter testing + // @ts-ignore + embeddableNoIndex.searchProps = { + onFilter: async (field: any, value: any, operator: any) => { + const indexPattern = mockSavedExploreNoIndex.searchSource.getField('index'); + if (!indexPattern) return; + }, + }; + + // @ts-ignore + const searchProps = embeddableNoIndex.searchProps; + + // Test onFilter returns early without calling executeTriggerActions + await searchProps?.onFilter?.({ name: 'field1' } as any, ['value1'], 'is'); + + // Check that executeTriggerActions was NOT called + expect(mockExecuteTriggerActionsLocal).not.toHaveBeenCalled(); + }); + + test('renders successfully even without index pattern', () => { + const mockServices = discoverPluginMock.createExploreServicesMock(); + mockServices.uiSettings.get = jest.fn().mockImplementation((key) => { + if (key === 'doc_table:hideTimeColumn') return false; + return 500; + }); + mockServices.data.query.queryString.getLanguageService = jest.fn().mockReturnValue({ + getLanguage: jest.fn().mockReturnValue({ + fields: { + formatter: jest.fn(), + }, + }), + }); + + // Create a new embeddable without index pattern const newEmbeddable = new ExploreEmbeddable( { savedExplore: { ...mockSavedExplore, searchSource: { ...mockSavedExplore.searchSource, - getField: jest.fn().mockReturnValue(null), + getField: jest.fn().mockImplementation((field) => { + if (field === 'query') return { query: 'test', language: 'PPL' }; + return null; + }), }, }, editUrl: '/app/explore/logs/test', editPath: 'test', indexPatterns: [], editable: true, - filterManager: {} as any, - services: {} as any, + filterManager: mockServices.filterManager, + services: mockServices, editApp: 'explore/logs', }, mockInput, mockExecuteTriggerActions ); - // Expect render to throw an error - expect(() => newEmbeddable.render(mockNode)).toThrow('Search scope not defined'); + // searchProps should be initialized even without index pattern + // @ts-ignore + expect(newEmbeddable.searchProps).toBeDefined(); + + // Render should work without throwing + expect(() => newEmbeddable.render(mockNode)).not.toThrow(); }); test('constructor handles missing indexPattern gracefully', () => { + const mockServices = discoverPluginMock.createExploreServicesMock(); + mockServices.uiSettings.get = jest.fn().mockImplementation((key) => { + if (key === 'doc_table:hideTimeColumn') return false; + return 500; + }); + mockServices.data.query.queryString.getLanguageService = jest.fn().mockReturnValue({ + getLanguage: jest.fn().mockReturnValue({ + fields: { + formatter: jest.fn(), + }, + }), + }); + const mockSavedExploreNoIndex = { ...mockSavedExplore, searchSource: { @@ -404,16 +498,17 @@ describe('ExploreEmbeddable', () => { editPath: 'test', indexPatterns: [], editable: true, - filterManager: {} as any, - services: {} as any, + filterManager: mockServices.filterManager, + services: mockServices, editApp: 'explore/logs', }, mockInput, mockExecuteTriggerActions ); - // @ts-ignore - expect(embeddableNoIndex.searchProps).toBeUndefined(); - expect(() => embeddableNoIndex.render(mockNode)).toThrow('Search scope not defined'); + // @ts-ignore - searchProps should now be defined even without indexPattern + expect(embeddableNoIndex.searchProps).toBeDefined(); + // @ts-ignore - indexPattern should be null/undefined + expect(embeddableNoIndex.searchProps?.indexPattern).toBeNull(); }); test('onAddColumn/onRemoveColumn/onMoveColumn/onSetColumns handle undefined columns gracefully', () => { @@ -469,21 +564,6 @@ describe('ExploreEmbeddable', () => { expect(() => embeddable.destroy()).not.toThrow(); }); - test('fetch throws error when no matchedRule is exist', async () => { - jest.spyOn(visualizationRegistry, 'findRuleByAxesMapping').mockReturnValueOnce(undefined); - - mockSavedExplore.visualization = JSON.stringify({ - chartType: 'line', - axesMapping: { x: 'field1', y: 'field2' }, - }); - mockSavedExplore.uiState = JSON.stringify({ activeTab: 'visualization' }); - - // @ts-ignore - await expect(embeddable.fetch()).rejects.toThrow( - 'Cannot load saved visualization "Test Explore" with id test-id' - ); - }); - test('fetch handles empty data by skipping visualization processing', async () => { const mockNormalizeResultRows = await import( '../components/visualizations/utils/normalize_result_rows' diff --git a/src/plugins/explore/public/embeddable/explore_embeddable.tsx b/src/plugins/explore/public/embeddable/explore_embeddable.tsx index 5dae7aff9b19..7713688378c6 100644 --- a/src/plugins/explore/public/embeddable/explore_embeddable.tsx +++ b/src/plugins/explore/public/embeddable/explore_embeddable.tsx @@ -171,7 +171,6 @@ export class ExploreEmbeddable private initializeSearchProps() { const { searchSource } = this.savedExplore; const indexPattern = searchSource.getField('index'); - if (!indexPattern) return; const searchProps: SearchProps = { inspectorAdapters: this.inspectorAdaptors, rows: [], @@ -254,7 +253,7 @@ export class ExploreEmbeddable field, value, operator, - indexPattern.id! + indexPattern?.id! ); filters = filters.map((filter) => ({ ...filter, @@ -421,6 +420,11 @@ export class ExploreEmbeddable // NOTE: PPL response is not the same as OpenSearch response, resp.hits.total here is 0. this.searchProps.hits = resp.hits.hits.length; this.searchProps.isLoading = false; + + // Render the component with updated data + if (this.node && this.searchProps) { + this.renderComponent(this.node, this.searchProps); + } }; private renderComponent(node: HTMLElement, searchProps: SearchProps) { @@ -466,6 +470,7 @@ export class ExploreEmbeddable ReactDOM.unmountComponentAtNode(this.node); } this.node = node; + this.node.style.height = '100%'; } public getInspectorAdapters() { diff --git a/src/plugins/explore/public/embeddable/explore_embeddable_factory.test.tsx b/src/plugins/explore/public/embeddable/explore_embeddable_factory.test.tsx index 0360141296cf..043fc2e6fa67 100644 --- a/src/plugins/explore/public/embeddable/explore_embeddable_factory.test.tsx +++ b/src/plugins/explore/public/embeddable/explore_embeddable_factory.test.tsx @@ -86,11 +86,136 @@ describe('ExploreEmbeddableFactory', () => { expect(mockStartServices.isEditable).toHaveBeenCalled(); }); - test('create returns an error object', async () => { + test('create returns an error when attributes are missing', async () => { const input = { id: 'test', timeRange: { from: 'now-15m', to: 'now' } }; const result = await factory.create(input as any); - // Check that the result is an error object (has an error property) - expect(result).toHaveProperty('error'); + // Check that the result is an ErrorEmbeddable + expect(result).toBeInstanceOf(ErrorEmbeddable); + }); + + test('create successfully creates by-value embeddable with attributes', async () => { + const mockSearchSource = { + getField: jest.fn((field) => { + if (field === 'index') return { id: 'test-index' }; + return null; + }), + }; + + jest.spyOn(OsdServices, 'getServices').mockReturnValue({ + ...mockedGetServicesResults, + data: { + search: { + searchSource: { + create: jest.fn().mockResolvedValue(mockSearchSource), + }, + }, + }, + } as any); + + const input = { + id: 'test', + timeRange: { from: 'now-15m', to: 'now' }, + attributes: { + title: 'Test Explore', + description: 'Test description', + type: 'logs', + columns: ['column1'], + sort: [['column1', 'asc']], + visualization: JSON.stringify({ chartType: 'bar' }), + uiState: JSON.stringify({ activeTab: 'visualization' }), + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ query: { query: '', language: 'PPL' } }), + }, + }, + references: [], + }; + + const result = await factory.create(input as any); + + expect(ExploreEmbeddable).toHaveBeenCalledWith( + expect.objectContaining({ + savedExplore: expect.objectContaining({ + id: 'test', + title: 'Test Explore', + }), + editUrl: '', + editPath: '', + editable: false, + }), + input, + mockStartServices.executeTriggerActions, + undefined + ); + }); + + test('create handles undefined index pattern in by-value embeddable', async () => { + const mockSearchSource = { + getField: jest.fn(() => null), + }; + + jest.spyOn(OsdServices, 'getServices').mockReturnValue({ + ...mockedGetServicesResults, + data: { + search: { + searchSource: { + create: jest.fn().mockResolvedValue(mockSearchSource), + }, + }, + }, + } as any); + + const input = { + id: 'test', + timeRange: { from: 'now-15m', to: 'now' }, + attributes: { + title: 'Test Explore', + type: 'logs', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ query: { query: '', language: 'PPL' } }), + }, + }, + references: [], + }; + + const result = await factory.create(input as any); + + expect(ExploreEmbeddable).toHaveBeenCalledWith( + expect.objectContaining({ + indexPatterns: [], + }), + input, + mockStartServices.executeTriggerActions, + undefined + ); + }); + + test('create returns error embeddable on exception', async () => { + jest.spyOn(OsdServices, 'getServices').mockReturnValue({ + ...mockedGetServicesResults, + data: { + search: { + searchSource: { + create: jest.fn().mockRejectedValue(new Error('Test error')), + }, + }, + }, + } as any); + + const input = { + id: 'test', + timeRange: { from: 'now-15m', to: 'now' }, + attributes: { + title: 'Test Explore', + type: 'logs', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ query: { query: '', language: 'PPL' } }), + }, + }, + references: [], + }; + + const result = await factory.create(input as any); + expect(result).toBeInstanceOf(ErrorEmbeddable); }); test('createFromSavedObject creates an ExploreEmbeddable', async () => { diff --git a/src/plugins/explore/public/embeddable/explore_embeddable_factory.tsx b/src/plugins/explore/public/embeddable/explore_embeddable_factory.tsx index b9996b97e615..e35dd580a4b6 100644 --- a/src/plugins/explore/public/embeddable/explore_embeddable_factory.tsx +++ b/src/plugins/explore/public/embeddable/explore_embeddable_factory.tsx @@ -14,12 +14,17 @@ import { Container, ErrorEmbeddable, } from '../../../embeddable/public'; -import { TimeRange } from '../../../data/public'; +import { + TimeRange, + injectSearchSourceReferences, + parseSearchSourceJSON, +} from '../../../data/public'; import { ExploreInput, ExploreOutput } from './types'; import { EXPLORE_EMBEDDABLE_TYPE } from './constants'; import { ExploreEmbeddable } from './explore_embeddable'; import { VisualizationRegistryService } from '../services/visualization_registry_service'; import { ExploreFlavor } from '../../common'; +import { SavedExplore } from '../saved_explore'; interface StartServices { executeTriggerActions: UiActionsStart['executeTriggerActions']; @@ -113,7 +118,61 @@ export class ExploreEmbeddableFactory } }; - public async create(input: ExploreInput) { - return new ErrorEmbeddable('Saved explores can only be created from a saved object', input); + /** + * Creates a by-value explore embeddable from input without a stored saved object. + */ + public async create( + input: ExploreInput, + parent?: Container + ): Promise { + if (!input.attributes) { + return new ErrorEmbeddable( + 'Attributes are required. Use createFromSavedObject to create from a saved object id', + input, + parent + ); + } + + const services = getServices(); + const filterManager = services.filterManager; + const attributes = input.attributes; + const references = input.references || []; + + try { + let searchSourceValues = parseSearchSourceJSON( + attributes.kibanaSavedObjectMeta!.searchSourceJSON + ); + searchSourceValues = injectSearchSourceReferences(searchSourceValues, references); + const searchSource = await services.data.search.searchSource.create(searchSourceValues); + const indexPattern = searchSource.getField('index'); + + const savedExplore = { + id: input.id, + ...input.attributes, + searchSource, + } as SavedExplore; + + const { executeTriggerActions } = await this.getStartServices(); + const { ExploreEmbeddable: ExploreEmbeddableClass } = await import('./explore_embeddable'); + const flavor = savedExplore.type; + + return new ExploreEmbeddableClass( + { + savedExplore, + editUrl: '', // by-value embeddables cannot be edited + editPath: '', + filterManager, + editable: false, + indexPatterns: indexPattern ? [indexPattern] : [], + services, + editApp: `explore/${flavor}`, + }, + input, + executeTriggerActions, + parent + ); + } catch (e) { + return new ErrorEmbeddable(e, input, parent); + } } } diff --git a/src/plugins/explore/public/embeddable/types.ts b/src/plugins/explore/public/embeddable/types.ts index f1ff8b14a4ff..53761081aca7 100644 --- a/src/plugins/explore/public/embeddable/types.ts +++ b/src/plugins/explore/public/embeddable/types.ts @@ -3,10 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { SavedObjectReference } from 'opensearch-dashboards/public'; import { Embeddable, EmbeddableInput, EmbeddableOutput } from '../../../embeddable/public'; import { Filter, IIndexPattern, TimeRange } from '../../../data/public'; import { QueryState } from '../application/utils/state_management/slices'; -import { SortOrder } from '../types/saved_explore_types'; +import { SortOrder, SavedExplore } from '../types/saved_explore_types'; export interface ExploreInput extends EmbeddableInput { timeRange: TimeRange; @@ -15,6 +16,9 @@ export interface ExploreInput extends EmbeddableInput { hidePanelTitles?: boolean; columns?: string[]; sort?: SortOrder[]; + // attributes and references are used to create embeddables without storing saved object + attributes?: ExploreByValueAttributes; + references?: SavedObjectReference[]; } export interface ExploreOutput extends EmbeddableOutput { @@ -26,3 +30,15 @@ export interface ExploreOutput extends EmbeddableOutput { export interface ExploreEmbeddable extends Embeddable { type: string; } + +type ExploreByValueAttributes = Pick< + SavedExplore, + | 'title' + | 'description' + | 'columns' + | 'sort' + | 'type' + | 'visualization' + | 'uiState' + | 'kibanaSavedObjectMeta' +>; From c5d8a9860d49b2bd67b551110933fd16e5b9322a Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:44:11 +0000 Subject: [PATCH 2/3] Changeset file for PR #10970 created/updated --- changelogs/fragments/10970.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/10970.yml diff --git a/changelogs/fragments/10970.yml b/changelogs/fragments/10970.yml new file mode 100644 index 000000000000..f4f46d009e6c --- /dev/null +++ b/changelogs/fragments/10970.yml @@ -0,0 +1,2 @@ +feat: +- Support by-value explore embeddables without stored saved object ([#10970](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10970)) \ No newline at end of file From 62a8d9db66507d29b8dca6c0f1d5cc122e880b9a Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Wed, 26 Nov 2025 00:54:12 +0000 Subject: [PATCH 3/3] address comments Signed-off-by: Joshua Li --- src/plugins/explore/public/embeddable/explore_embeddable.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/plugins/explore/public/embeddable/explore_embeddable.tsx b/src/plugins/explore/public/embeddable/explore_embeddable.tsx index 7713688378c6..93921a8b1484 100644 --- a/src/plugins/explore/public/embeddable/explore_embeddable.tsx +++ b/src/plugins/explore/public/embeddable/explore_embeddable.tsx @@ -420,11 +420,6 @@ export class ExploreEmbeddable // NOTE: PPL response is not the same as OpenSearch response, resp.hits.total here is 0. this.searchProps.hits = resp.hits.hits.length; this.searchProps.isLoading = false; - - // Render the component with updated data - if (this.node && this.searchProps) { - this.renderComponent(this.node, this.searchProps); - } }; private renderComponent(node: HTMLElement, searchProps: SearchProps) {