diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb066f49315..8acb37a43565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,37 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 📈 Features/Enhancements - [Multiple Datasource] Add TLS configuration for multiple data sources ([#6171](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6171)) +<<<<<<< HEAD +======= +- [Multiple Datasource] Add multi selectable data source component ([#6211](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6211)) +- [Multiple Datasource] Do not support import data source object to Local cluster when not enable data source ([#6395](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6395)) +- [Multiple Datasource] Refactor data source menu and interface to allow cleaner selection of component and related configurations ([#6256](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6256)) +- [Multiple Datasource] Allow top nav menu to mount data source menu for use case when both menus are mounted ([#6268](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6268)) +- [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179)) +- [Workspace] Add update workspace page ([#6270](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6270)) +- [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) +- [Workspace] Add workspace list page ([#6182](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6182)) +- [Workspace] Add API to duplicate saved objects among workspaces ([#6288](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6288)) +- [Workspace] Add workspaces column to saved objects page ([#6225](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6225)) +- [Multiple Datasource] Enhanced data source selector with default datasource shows as first choice ([#6293](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6293)) +- [Multiple Datasource] Add multi data source support to sample vega visualizations ([#6218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6218)) +- [Multiple Datasource] Fetch data source title for DataSourceView when only id is provided ([#6315](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6315) +- [Multiple Datasource] Get data source label when only id is provided in DataSourceSelectable ([#6358](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6358) +- [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052)) +- [Multiple Datasource] Add default icon for selectable component and make sure the default datasource shows automatically ([#6327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6327)) +- [Multiple Datasource] Pass selected data sources to plugin consumers when the multi-select component initially loads ([#6333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6333)) +- [Multiple Datasource] Add installedPlugins list to data source saved object ([#6348](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6348)) +- [Multiple Datasource] Add default icon in multi-selectable picker ([#6357](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6357)) +- [Workspace] Add APIs to support plugin state in request ([#6303](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6303)) +- [Workspace] Filter left nav menu items according to the current workspace ([#6234](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6234)) +- [Multiple Datasource] Refactor data source selector component to include placeholder and add tests ([#6372](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6372)) +- Replace control characters before logging ([#6402](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6402)) +- [Dynamic Configurations] Improve dynamic configurations by adding cache and simplifying client fetch ([#6364](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6364)) +- [CSP Handler] Update CSP handler to only query and modify frame ancestors instead of all CSP directives ([#6398](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6398)) +- [MD] Add OpenSearch cluster group label to top of single selectable dropdown ([#6400](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6400)) +- [Workspace] Support workspace in saved objects client in server side. ([#6365](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6365)) +- [MD] Add dropdown header to data source single selector ([#6431](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6431)) +>>>>>>> 97355995c8... [Multiple DataSource] Do not support import data source object to Local cluster when not enable data source (#6395) ### 🐛 Bug Fixes diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index fff5b60c89cc..ea4ac66b1a0b 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -96,11 +96,12 @@ describe('#importSavedObjectsFromStream', () => { let savedObjectsClient: jest.Mocked; let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const testDataSourceId = 'some-datasource'; + const testDataSourceId = uuidv4(); const setupOptions = ( createNewCopies: boolean = false, - dataSourceId: string | undefined = undefined + dataSourceId: string | undefined = undefined, + dataSourceEnabled: boolean | undefined = false ): SavedObjectsImportOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); @@ -135,6 +136,17 @@ describe('#importSavedObjectsFromStream', () => { attributes: { title: 'some-title' }, }; }; + + const createDataSourceObject = (): SavedObject<{ + title: string; + }> => { + return { + type: 'data-source', + id: uuidv4(), + references: [], + attributes: { title: 'some-title' }, + }; + }; const createError = (): SavedObjectsImportError => { const title = 'some-title'; return { @@ -589,5 +601,94 @@ describe('#importSavedObjectsFromStream', () => { const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); }); + + test('early return if import data source objects to non-MDS cluster', async () => { + const options = setupOptions(false, testDataSourceId, false); + const dsObj = createDataSourceObject(); + const dsExportedObj = createObject(testDataSourceId); + const collectedObjects = [dsObj, dsExportedObj]; + + const errors = [ + { + type: dsObj.type, + id: dsObj.id, + title: dsObj.attributes.title, + meta: { title: dsObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + { + type: dsExportedObj.type, + id: dsExportedObj.id, + title: dsExportedObj.attributes.title, + meta: { title: dsExportedObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + ]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + const result = await importSavedObjectsFromStream(options); + const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); + expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + }); + + test('early return if import mixed non/data source objects to non-MDS cluster', async () => { + const options = setupOptions(false, testDataSourceId, false); + const dsObj = createDataSourceObject(); + const dsExportedObj = createObject(testDataSourceId); + const nonDsExportedObj = createObject(); + const collectedObjects = [dsObj, dsExportedObj, nonDsExportedObj]; + + const errors = [ + { + type: dsObj.type, + id: dsObj.id, + title: dsObj.attributes.title, + meta: { title: dsObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + { + type: dsExportedObj.type, + id: dsExportedObj.id, + title: dsExportedObj.attributes.title, + meta: { title: dsExportedObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + ]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + const result = await importSavedObjectsFromStream(options); + const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); + expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + }); + + test('early return if import single data source objects to non-MDS cluster', async () => { + const options = setupOptions(false, testDataSourceId, false); + const dsObj = createDataSourceObject(); + const collectedObjects = [dsObj]; + + const errors = [ + { + type: dsObj.type, + id: dsObj.id, + title: dsObj.attributes.title, + meta: { title: dsObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + ]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + const result = await importSavedObjectsFromStream(options); + const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); + expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + }); }); }); diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index e82b4e634e0f..cfd091149004 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -33,6 +33,7 @@ import { SavedObjectsImportError, SavedObjectsImportResponse, SavedObjectsImportOptions, + SavedObjectsImportUnsupportedTypeError, } from './types'; import { validateReferences } from './validate_references'; import { checkOriginConflicts } from './check_origin_conflicts'; @@ -40,6 +41,7 @@ import { createSavedObjects } from './create_saved_objects'; import { checkConflicts } from './check_conflicts'; import { regenerateIds } from './regenerate_ids'; import { checkConflictsForDataSource } from './check_conflict_for_data_source'; +import { isSavedObjectWithDataSource } from './validate_object_id'; /** * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more @@ -58,6 +60,7 @@ export async function importSavedObjectsFromStream({ dataSourceId, dataSourceTitle, workspaces, + dataSourceEnabled, }: SavedObjectsImportOptions): Promise { let errorAccumulator: SavedObjectsImportError[] = []; const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); @@ -69,6 +72,28 @@ export async function importSavedObjectsFromStream({ supportedTypes, dataSourceId, }); + // if not enable data_source, throw error early + if (!dataSourceEnabled) { + const notSupportedErrors: SavedObjectsImportError[] = collectSavedObjectsResult.collectedObjects.reduce( + (errors: SavedObjectsImportError[], obj) => { + if (obj.type === 'data-source' || isSavedObjectWithDataSource(obj.id)) { + const error: SavedObjectsImportUnsupportedTypeError = { type: 'unsupported_type' }; + const { title } = obj.attributes; + errors.push({ error, type: obj.type, id: obj.id, title, meta: { title } }); + } + return errors; // Return the accumulator in each iteration + }, + [] + ); + if (notSupportedErrors?.length > 0) { + return { + successCount: 0, + success: false, + errors: notSupportedErrors, + }; + } + } + errorAccumulator = [...errorAccumulator, ...collectSavedObjectsResult.errors]; /** Map of all IDs for objects that we are attempting to import; each value is empty by default */ let importIdMap = collectSavedObjectsResult.importIdMap; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 426d4cfde86c..8612870fcf56 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -189,7 +189,7 @@ export interface SavedObjectsImportOptions { createNewCopies: boolean; dataSourceId?: string; dataSourceTitle?: string; - /** if specified, will import in given workspaces */ + dataSourceEnabled?: boolean; workspaces?: SavedObjectsBaseOptions['workspaces']; } diff --git a/src/core/server/saved_objects/import/validate_object_id.test.ts b/src/core/server/saved_objects/import/validate_object_id.test.ts new file mode 100644 index 000000000000..2f0cb3c6487a --- /dev/null +++ b/src/core/server/saved_objects/import/validate_object_id.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isSavedObjectWithDataSource } from './validate_object_id'; + +describe('isObjectWithDataSource', () => { + test('should return false for valid object with data source ID but in wrong format', () => { + // Valid ID with two parts separated by underscore, and both parts being UUIDs + const inValidId = 'invalid_uuid_1234-invalid_uuid_5678'; + expect(isSavedObjectWithDataSource(inValidId)).toBe(false); + }); + + test('should return false for invalid IDs', () => { + // Missing underscore + const invalidId1 = 'missingunderscore'; + expect(isSavedObjectWithDataSource(invalidId1)).toBe(false); + + // Invalid UUID in the second part + const invalidId2 = 'valid_uuid_1234-invalid_uuid'; + expect(isSavedObjectWithDataSource(invalidId2)).toBe(false); + + // Missing second part + const invalidId3 = 'valid_uuid_1234'; + expect(isSavedObjectWithDataSource(invalidId3)).toBe(false); + + // More than two parts + const invalidId4 = 'valid_uuid_1234-valid_uuid_5678-extra_part'; + expect(isSavedObjectWithDataSource(invalidId4)).toBe(false); + }); + + test('should return false for non-UUID parts', () => { + // First part is not a UUID + const invalidId1 = 'not_a_uuid_valid_uuid_1234'; + expect(isSavedObjectWithDataSource(invalidId1)).toBe(false); + + // Second part is not a UUID + const invalidId2 = 'valid_uuid_1234_not_a_uuid'; + expect(isSavedObjectWithDataSource(invalidId2)).toBe(false); + + // Both parts are not UUIDs + const invalidId3 = 'not_a_uuid_not_a_uuid'; + expect(isSavedObjectWithDataSource(invalidId3)).toBe(false); + }); + + test('should return false for string with underscore but not with UUID', () => { + // First part is not a UUID + const invalidId = 'saved_object_with_index_pattern_conflict'; + expect(isSavedObjectWithDataSource(invalidId)).toBe(false); + }); + + test('should return false for string with underscore but with three UUIDs', () => { + // First part is not a UUID + const invalidId = + '7cbd2350-2223-11e8-b802-5bcf64c2cfb4_7cbd2350-2223-11e8-b802-5bcf64c2cfb4_7cbd2350-2223-11e8-b802-5bcf64c2cfb4'; + expect(isSavedObjectWithDataSource(invalidId)).toBe(false); + }); +}); diff --git a/src/core/server/saved_objects/import/validate_object_id.ts b/src/core/server/saved_objects/import/validate_object_id.ts new file mode 100644 index 000000000000..9a496c4d572a --- /dev/null +++ b/src/core/server/saved_objects/import/validate_object_id.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * When enable multiple data source, exported objects from a data source will maintain object id like + * "69a34b00-9ee8-11e7-8711-e7a007dcef99_7cbd2350-2223-11e8-b802-5bcf64c2cfb4" + * two UUIDs are connected with a underscore, + * before the underscore, the UUID represents the data source + * after the underscore, the UUID is the original object id + * when disable multiple data source, the exported object from local cluster will look like 7cbd2350-2223-11e8-b802-5bcf64c2cfb4 + * we can use this format to tell out whether a single object is exported from MDS enabled/disabled cluster + * + * This file to going to group some validate function to tell source of object based on the object id + */ + +/** + * + * @param candidate: string without underscore + * @returns + */ +const isUUID = (candidate: string): boolean => { + // Regular expression pattern for UUID + const uuidPattern: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidPattern.test(candidate); +}; + +/** + * + * @param id single object id + * @returns + */ +export const isSavedObjectWithDataSource = (id: string): boolean => { + const idParts = id.split('_'); + /** + * check with the + */ + return idParts && idParts.length === 2 && idParts.every(isUUID); +}; diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 1fc739ea168c..a2a5bdcacd7a 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -64,6 +64,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) workspaces: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) ), + dataSourceEnabled: schema.maybe(schema.boolean({ defaultValue: false })), }, { validate: (object) => { @@ -116,6 +117,8 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) workspaces = [workspaces]; } + const dataSourceEnabled = req.query.dataSourceEnabled; + const result = await importSavedObjectsFromStream({ savedObjectsClient: context.core.savedObjects.client, typeRegistry: context.core.savedObjects.typeRegistry, @@ -126,6 +129,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) dataSourceId, dataSourceTitle, workspaces, + dataSourceEnabled, }); return res.ok({ body: result }); diff --git a/src/plugins/saved_objects_management/public/lib/import_file.ts b/src/plugins/saved_objects_management/public/lib/import_file.ts index 3753a8251e10..f5156cd94f1d 100644 --- a/src/plugins/saved_objects_management/public/lib/import_file.ts +++ b/src/plugins/saved_objects_management/public/lib/import_file.ts @@ -41,7 +41,8 @@ export async function importFile( http: HttpStart, file: File, { createNewCopies, overwrite }: ImportMode, - selectedDataSourceId?: string + selectedDataSourceId?: string, + dataSourceEnabled?: boolean ) { const formData = new FormData(); formData.append('file', file); @@ -49,6 +50,9 @@ export async function importFile( if (selectedDataSourceId) { query.dataSourceId = selectedDataSourceId; } + if (dataSourceEnabled) { + query.dataSourceEnabled = dataSourceEnabled; + } return await http.post('/api/saved_objects/_import', { body: formData, headers: { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 4237582ea53a..d68f17a64f6c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -221,6 +221,7 @@ describe('Flyout', () => { createNewCopies: true, overwrite: true, }, + undefined, undefined ); expect(component.state()).toMatchObject({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index b2542c4a56c0..fb2f98855364 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -195,13 +195,19 @@ export class Flyout extends Component { * Does the initial import of a file, resolveImportErrors then handles errors and retries */ import = async () => { - const { http } = this.props; + const { http, dataSourceEnabled } = this.props; const { file, importMode, selectedDataSourceId } = this.state; this.setState({ status: 'loading', error: undefined }); // Import the file try { - const response = await importFile(http, file!, importMode, selectedDataSourceId); + const response = await importFile( + http, + file!, + importMode, + selectedDataSourceId, + dataSourceEnabled + ); this.setState(processImportResponse(response), () => { // Resolve import errors right away if there's no index patterns to match // This will ask about overwriting each object, etc