Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/10970.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Support by-value explore embeddables without stored saved object ([#10970](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10970))
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
134 changes: 107 additions & 27 deletions src/plugins/explore/public/embeddable/explore_embeddable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down Expand Up @@ -254,7 +253,7 @@ export class ExploreEmbeddable
field,
value,
operator,
indexPattern.id!
indexPattern?.id!
);
filters = filters.map((filter) => ({
...filter,
Expand Down Expand Up @@ -466,6 +465,7 @@ export class ExploreEmbeddable
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
this.node.style.height = '100%';
}

public getInspectorAdapters() {
Expand Down
Loading
Loading