Skip to content

Commit af6ca5f

Browse files
ananzhopensearch-changeset-bot[bot]LDrago27
authored
[Bug][Datasets] Fix missing data source information in dataset selector for namespaced index patterns (#10844)
* [Bug] Fix missing data source information in dataset selector for namespaced index patterns Signed-off-by: Anan Zhuang <[email protected]> * Changeset file for PR #10844 created/updated * Fix Failing Test Signed-off-by: Suchit Sahoo <[email protected]> --------- Signed-off-by: Anan Zhuang <[email protected]> Signed-off-by: Suchit Sahoo <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Suchit Sahoo <[email protected]>
1 parent 12a5133 commit af6ca5f

File tree

4 files changed

+243
-5
lines changed

4 files changed

+243
-5
lines changed

changelogs/fragments/10844.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fix:
2+
- [Bug] Fix missing data source information in dataset selector for namespaced index patterns ([#10844](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10844))

cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/explore/01/saved_search.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const runSavedSearchTests = () => {
5555
cy.visit(`/w/${workspaceId}/app/data-explorer/discover`);
5656
});
5757

58-
cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType, true);
58+
cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType);
5959
cy.osd.grabIdsFromDiscoverPageUrl();
6060

6161
// using a POST request to create a saved search to load

src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.test.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { indexPatternTypeConfig } from './index_pattern_type';
99
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
1010
import { DATA_STRUCTURE_META_TYPES, DataStructure, Dataset } from '../../../../../common';
1111
import * as services from '../../../../services';
12+
import * as utilsModule from './utils';
1213

1314
jest.mock('../../../../services', () => ({
1415
getIndexPatterns: jest.fn(),
@@ -113,4 +114,221 @@ describe('indexPatternTypeConfig', () => {
113114
'SQL',
114115
]);
115116
});
117+
118+
describe('fetch', () => {
119+
beforeEach(() => {
120+
(utilsModule.injectMetaToDataStructures as jest.Mock).mockImplementation(
121+
(structures: DataStructure[]) => structures
122+
);
123+
});
124+
125+
test('should extract data source from references array (traditional method)', async () => {
126+
const client = ({
127+
find: jest.fn().mockResolvedValue({
128+
savedObjects: [
129+
{
130+
id: 'pattern-123',
131+
type: 'index-pattern',
132+
attributes: {
133+
title: 'my-pattern',
134+
timeFieldName: '@timestamp',
135+
},
136+
references: [{ id: 'datasource-abc', type: 'data-source', name: 'dataSource' }],
137+
},
138+
],
139+
}),
140+
bulkGet: jest.fn().mockResolvedValue({
141+
savedObjects: [
142+
{
143+
id: 'datasource-abc',
144+
type: 'data-source',
145+
attributes: { title: 'My Data Source', dataSourceEngineType: 'OpenSearch' },
146+
},
147+
],
148+
}),
149+
} as unknown) as SavedObjectsClientContract;
150+
151+
// @ts-expect-error - Partial mock for testing
152+
const result = await indexPatternTypeConfig.fetch({ savedObjects: { client } }, []);
153+
154+
expect(client.find).toHaveBeenCalledWith({
155+
type: 'index-pattern',
156+
fields: ['title', 'timeFieldName', 'references'],
157+
search: '*',
158+
searchFields: ['title'],
159+
perPage: 100,
160+
});
161+
162+
expect(client.bulkGet).toHaveBeenCalledWith([{ id: 'datasource-abc', type: 'data-source' }]);
163+
164+
expect(result.children).toHaveLength(1);
165+
expect(result.children![0]).toEqual({
166+
id: 'pattern-123',
167+
title: 'my-pattern',
168+
type: 'INDEX_PATTERN',
169+
meta: {
170+
type: DATA_STRUCTURE_META_TYPES.CUSTOM,
171+
timeFieldName: '@timestamp',
172+
},
173+
parent: {
174+
id: 'datasource-abc',
175+
title: 'My Data Source',
176+
type: 'OpenSearch',
177+
},
178+
});
179+
});
180+
181+
test('should extract data source from namespaced ID when references are empty', async () => {
182+
const client = ({
183+
find: jest.fn().mockResolvedValue({
184+
savedObjects: [
185+
{
186+
id: 'datasource-xyz::my-pattern',
187+
type: 'index-pattern',
188+
attributes: {
189+
title: 'my-pattern',
190+
timeFieldName: '@timestamp',
191+
},
192+
references: [],
193+
},
194+
],
195+
}),
196+
bulkGet: jest.fn().mockResolvedValue({
197+
savedObjects: [
198+
{
199+
id: 'datasource-xyz',
200+
type: 'data-source',
201+
attributes: { title: 'External Data Source', dataSourceEngineType: 'OpenSearch' },
202+
},
203+
],
204+
}),
205+
} as unknown) as SavedObjectsClientContract;
206+
207+
// @ts-expect-error - Partial mock for testing
208+
const result = await indexPatternTypeConfig.fetch({ savedObjects: { client } }, []);
209+
210+
expect(client.bulkGet).toHaveBeenCalledWith([{ id: 'datasource-xyz', type: 'data-source' }]);
211+
212+
expect(result.children).toHaveLength(1);
213+
expect(result.children![0]).toEqual({
214+
id: 'datasource-xyz::my-pattern',
215+
title: 'my-pattern',
216+
type: 'INDEX_PATTERN',
217+
meta: {
218+
type: DATA_STRUCTURE_META_TYPES.CUSTOM,
219+
timeFieldName: '@timestamp',
220+
},
221+
parent: {
222+
id: 'datasource-xyz',
223+
title: 'External Data Source',
224+
type: 'OpenSearch',
225+
},
226+
});
227+
});
228+
229+
test('should handle index patterns without data source', async () => {
230+
const client = ({
231+
find: jest.fn().mockResolvedValue({
232+
savedObjects: [
233+
{
234+
id: 'pattern-456',
235+
type: 'index-pattern',
236+
attributes: {
237+
title: 'local-pattern',
238+
timeFieldName: '@timestamp',
239+
},
240+
references: [],
241+
},
242+
],
243+
}),
244+
bulkGet: jest.fn().mockResolvedValue({ savedObjects: [] }),
245+
} as unknown) as SavedObjectsClientContract;
246+
247+
// @ts-expect-error - Partial mock for testing
248+
const result = await indexPatternTypeConfig.fetch({ savedObjects: { client } }, []);
249+
250+
// bulkGet should not be called when there are no data sources
251+
expect(client.bulkGet).not.toHaveBeenCalled();
252+
253+
expect(result.children).toHaveLength(1);
254+
expect(result.children![0]).toEqual({
255+
id: 'pattern-456',
256+
title: 'local-pattern',
257+
type: 'INDEX_PATTERN',
258+
meta: {
259+
type: DATA_STRUCTURE_META_TYPES.CUSTOM,
260+
timeFieldName: '@timestamp',
261+
},
262+
});
263+
expect(result.children![0].parent).toBeUndefined();
264+
});
265+
266+
test('should handle mixed scenarios with both traditional and namespaced methods', async () => {
267+
const client = ({
268+
find: jest.fn().mockResolvedValue({
269+
savedObjects: [
270+
{
271+
id: 'pattern-traditional',
272+
type: 'index-pattern',
273+
attributes: { title: 'traditional-pattern', timeFieldName: '@timestamp' },
274+
references: [{ id: 'datasource-1', type: 'data-source', name: 'dataSource' }],
275+
},
276+
{
277+
id: 'datasource-2::namespaced-pattern',
278+
type: 'index-pattern',
279+
attributes: { title: 'namespaced-pattern', timeFieldName: '@timestamp' },
280+
references: [],
281+
},
282+
{
283+
id: 'local-pattern',
284+
type: 'index-pattern',
285+
attributes: { title: 'local-only', timeFieldName: '@timestamp' },
286+
references: [],
287+
},
288+
],
289+
}),
290+
bulkGet: jest.fn().mockResolvedValue({
291+
savedObjects: [
292+
{
293+
id: 'datasource-1',
294+
type: 'data-source',
295+
attributes: { title: 'Data Source 1', dataSourceEngineType: 'OpenSearch' },
296+
},
297+
{
298+
id: 'datasource-2',
299+
type: 'data-source',
300+
attributes: { title: 'Data Source 2', dataSourceEngineType: 'OpenSearch' },
301+
},
302+
],
303+
}),
304+
} as unknown) as SavedObjectsClientContract;
305+
306+
// @ts-expect-error - Partial mock for testing
307+
const result = await indexPatternTypeConfig.fetch({ savedObjects: { client } }, []);
308+
309+
expect(client.bulkGet).toHaveBeenCalledWith([
310+
{ id: 'datasource-1', type: 'data-source' },
311+
{ id: 'datasource-2', type: 'data-source' },
312+
]);
313+
314+
expect(result.children).toHaveLength(3);
315+
316+
// Traditional method
317+
expect(result.children![0].parent).toEqual({
318+
id: 'datasource-1',
319+
title: 'Data Source 1',
320+
type: 'OpenSearch',
321+
});
322+
323+
// Namespaced method
324+
expect(result.children![1].parent).toEqual({
325+
id: 'datasource-2',
326+
title: 'Data Source 2',
327+
type: 'OpenSearch',
328+
});
329+
330+
// No data source
331+
expect(result.children![2].parent).toBeUndefined();
332+
});
333+
});
116334
});

src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,23 @@ const fetchIndexPatterns = async (client: SavedObjectsClientContract): Promise<D
105105
perPage: 100,
106106
});
107107

108-
// Get all unique data source ids
108+
// Get all unique data source ids from both references and index pattern IDs
109109
const datasourceIds = Array.from(
110110
new Set(
111111
resp.savedObjects
112-
.filter((savedObject) => savedObject.references.length > 0)
113-
.map((savedObject) => savedObject.references.find((ref) => ref.type === 'data-source')?.id)
112+
.map((savedObject) => {
113+
// First try to get from references
114+
const refDataSourceId = savedObject.references.find((ref) => ref.type === 'data-source')
115+
?.id;
116+
if (refDataSourceId) {
117+
return refDataSourceId;
118+
}
119+
// If not in references, check if the ID contains :: (namespaced format)
120+
if (savedObject.id.includes('::')) {
121+
return savedObject.id.split('::')[0];
122+
}
123+
return undefined;
124+
})
114125
.filter(Boolean)
115126
)
116127
) as string[];
@@ -128,7 +139,14 @@ const fetchIndexPatterns = async (client: SavedObjectsClientContract): Promise<D
128139

129140
const dataStructures = resp.savedObjects.map(
130141
(savedObject): DataStructure => {
131-
const dataSourceId = savedObject.references.find((ref) => ref.type === 'data-source')?.id;
142+
// First try to get dataSourceId from references
143+
let dataSourceId = savedObject.references.find((ref) => ref.type === 'data-source')?.id;
144+
145+
// If not in references, check if the ID contains :: (namespaced format)
146+
if (!dataSourceId && savedObject.id.includes('::')) {
147+
dataSourceId = savedObject.id.split('::')[0];
148+
}
149+
132150
const dataSource = dataSourceId ? dataSourceMap[dataSourceId] : undefined;
133151

134152
const indexPatternDataStructure: DataStructure = {

0 commit comments

Comments
 (0)