Skip to content

Commit a1d2e7b

Browse files
[BUG] Allow workspace-specific datasets with same title by using UUID (#10899)
* [BUG] Allow workspace-specific datasets with same title by using UUID generation Signed-off-by: Anan Zhuang <[email protected]> * fix PR comment Signed-off-by: Anan Zhuang <[email protected]> * Changeset file for PR #10899 created/updated --------- Signed-off-by: Anan Zhuang <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent 06a1ba8 commit a1d2e7b

File tree

3 files changed

+142
-5
lines changed

3 files changed

+142
-5
lines changed

changelogs/fragments/10899.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fix:
2+
- [BUG] Allow workspace-specific datasets with same title by using UUID ([#10899](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10899))

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

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('DatasetService', () => {
207207
);
208208
});
209209

210-
test('saveDataset creates and saves a new dataset', async () => {
210+
test('saveDataset creates and saves a new dataset without data source', async () => {
211211
const mockDataset = {
212212
id: 'test-dataset',
213213
title: 'Test Dataset',
@@ -216,10 +216,15 @@ describe('DatasetService', () => {
216216
type: mockType.id,
217217
timeFieldName: 'timestamp',
218218
schemaMappings: { otelLogs: { spanId: 'span.id' } },
219+
// No dataSource property
219220
} as Dataset;
220221

222+
const mockCreatedDataView = {
223+
id: 'generated-uuid-1234',
224+
};
225+
221226
const mockDataViews = {
222-
createAndSave: jest.fn().mockResolvedValue({}),
227+
createAndSave: jest.fn().mockResolvedValue(mockCreatedDataView),
223228
};
224229

225230
const servicesWithDataViews = {
@@ -233,19 +238,84 @@ describe('DatasetService', () => {
233238
service.registerType(mockType);
234239
await service.saveDataset(mockDataset, servicesWithDataViews, 'metrics');
235240

241+
// Should call createAndSave without the ID to allow UUID generation
236242
expect(mockDataViews.createAndSave).toHaveBeenCalledWith(
237243
expect.objectContaining({
238-
id: 'test-dataset',
244+
id: undefined, // Should be undefined for datasets without data source
245+
title: 'Test Dataset',
246+
displayName: 'My Dataset',
247+
description: 'Test description',
248+
timeFieldName: 'timestamp',
249+
signalType: 'metrics',
250+
schemaMappings: { otelLogs: { spanId: 'span.id' } },
251+
}),
252+
undefined,
253+
false
254+
);
255+
256+
// Should update the dataset with the generated UUID
257+
expect(mockDataset.id).toBe('generated-uuid-1234');
258+
});
259+
260+
test('saveDataset creates and saves a new dataset with data source', async () => {
261+
const mockDataset = {
262+
id: 'test-dataset',
263+
title: 'Test Dataset',
264+
displayName: 'My Dataset',
265+
description: 'Test description',
266+
type: mockType.id,
267+
timeFieldName: 'timestamp',
268+
schemaMappings: { otelLogs: { spanId: 'span.id' } },
269+
dataSource: {
270+
id: 'data-source-123',
271+
title: 'My Data Source',
272+
type: 'OpenSearch',
273+
version: '1.0',
274+
},
275+
} as Dataset;
276+
277+
const mockCreatedDataView = {
278+
id: 'data-source-123::generated-uuid-5678',
279+
};
280+
281+
const mockDataViews = {
282+
createAndSave: jest.fn().mockResolvedValue(mockCreatedDataView),
283+
};
284+
285+
const servicesWithDataViews = {
286+
...mockDataPluginServices,
287+
data: {
288+
...dataPluginMock.createStartContract(),
289+
dataViews: mockDataViews as any,
290+
},
291+
};
292+
293+
service.registerType(mockType);
294+
await service.saveDataset(mockDataset, servicesWithDataViews, 'metrics');
295+
296+
// Should call createAndSave with data source prefixed ID
297+
expect(mockDataViews.createAndSave).toHaveBeenCalledWith(
298+
expect.objectContaining({
299+
id: expect.stringMatching(/^data-source-123::[0-9a-f-]{36}$/), // Should match data-source-id::uuid pattern
239300
title: 'Test Dataset',
240301
displayName: 'My Dataset',
241302
description: 'Test description',
242303
timeFieldName: 'timestamp',
243304
signalType: 'metrics',
244305
schemaMappings: { otelLogs: { spanId: 'span.id' } },
306+
dataSourceRef: {
307+
id: 'data-source-123',
308+
name: 'My Data Source',
309+
type: 'OpenSearch',
310+
version: '1.0',
311+
},
245312
}),
246313
undefined,
247314
false
248315
);
316+
317+
// Should update the dataset with the generated UUID
318+
expect(mockDataset.id).toBe('data-source-123::generated-uuid-5678');
249319
});
250320

251321
test('saveDataset does not save index pattern datasets', async () => {
@@ -489,5 +559,59 @@ describe('DatasetService', () => {
489559
'Failed to save dataset'
490560
);
491561
});
562+
563+
test('does not update dataset ID when createAndSave returns undefined', async () => {
564+
const originalId = 'test-dataset';
565+
const mockDataset = {
566+
id: originalId,
567+
title: 'Test Dataset',
568+
type: mockType.id,
569+
} as Dataset;
570+
571+
const mockDataViews = {
572+
createAndSave: jest.fn().mockResolvedValue(undefined),
573+
};
574+
575+
const servicesWithDataViews = {
576+
...mockDataPluginServices,
577+
data: {
578+
...dataPluginMock.createStartContract(),
579+
dataViews: mockDataViews as any,
580+
},
581+
};
582+
583+
service.registerType(mockType);
584+
await service.saveDataset(mockDataset, servicesWithDataViews);
585+
586+
// Dataset ID should remain unchanged when no ID is returned
587+
expect(mockDataset.id).toBe(originalId);
588+
});
589+
590+
test('does not update dataset ID when createAndSave returns object without ID', async () => {
591+
const originalId = 'test-dataset';
592+
const mockDataset = {
593+
id: originalId,
594+
title: 'Test Dataset',
595+
type: mockType.id,
596+
} as Dataset;
597+
598+
const mockDataViews = {
599+
createAndSave: jest.fn().mockResolvedValue({}),
600+
};
601+
602+
const servicesWithDataViews = {
603+
...mockDataPluginServices,
604+
data: {
605+
...dataPluginMock.createStartContract(),
606+
dataViews: mockDataViews as any,
607+
},
608+
};
609+
610+
service.registerType(mockType);
611+
await service.saveDataset(mockDataset, servicesWithDataViews);
612+
613+
// Dataset ID should remain unchanged when no ID is returned
614+
expect(mockDataset.id).toBe(originalId);
615+
});
492616
});
493617
});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import { v4 as uuidv4 } from 'uuid';
67
import LRUCache from 'lru-cache';
78
import { CoreStart } from 'opensearch-dashboards/public';
89
import {
@@ -188,7 +189,8 @@ export class DatasetService {
188189
? ({} as IndexPatternFieldMap)
189190
: await type?.fetchFields(dataset, services);
190191
const spec = {
191-
id: dataset.id,
192+
// Generate ID with data source prefix if data source exists, otherwise allow UUID generation
193+
id: dataset.dataSource?.id ? `${dataset.dataSource.id}::${uuidv4()}` : undefined,
192194
displayName: dataset.displayName,
193195
title: dataset.title,
194196
timeFieldName: dataset.timeFieldName,
@@ -214,7 +216,16 @@ export class DatasetService {
214216
// Consider fetching fields after createAndSave and updating the saved object:
215217
// const dataView = await createAndSave(...);
216218
// if (asyncType) { await type.fetchFields(...); await dataViews.updateSavedObject(dataView); }
217-
await services.data?.dataViews.createAndSave(spec, undefined, asyncType);
219+
const createdDataView = await services.data?.dataViews.createAndSave(
220+
spec,
221+
undefined,
222+
asyncType
223+
);
224+
225+
// Update the dataset with the new UUID generated during save
226+
if (createdDataView?.id) {
227+
dataset.id = createdDataView.id;
228+
}
218229
}
219230
} catch (error) {
220231
// Re-throw DuplicateDataViewError without wrapping to preserve error type

0 commit comments

Comments
 (0)