From 97355995c89a78bf45ebbdcbe3364fa8cc6eafe6 Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:18:44 -0700 Subject: [PATCH 1/7] [Multiple DataSource] Do not support import data source object to Local cluster when not enable data source (#6395) * WIP resolve import ds to lc Signed-off-by: yujin-emma * add test Signed-off-by: yujin-emma * fix typo Signed-off-by: yujin-emma * fix failed test Signed-off-by: yujin-emma * Update CHANGELOG.md Signed-off-by: yujin-emma * fix the failed test Signed-off-by: yujin-emma * remove unused snapshot Signed-off-by: yujin-emma * Update src/core/server/saved_objects/import/validate_object_id.ts Co-authored-by: Zhongnan Su Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> * Update src/core/server/saved_objects/import/import_saved_objects.ts Co-authored-by: Zhongnan Su Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> * address comments Signed-off-by: yujin-emma * fix test Signed-off-by: yujin-emma * remove unused comments Signed-off-by: yujin-emma * fix bootstrap filure Signed-off-by: yujin-emma --------- Signed-off-by: yujin-emma Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Co-authored-by: Zhongnan Su --- CHANGELOG.md | 1 + .../import/import_saved_objects.test.ts | 105 +++++++++++++++++- .../import/import_saved_objects.ts | 25 +++++ src/core/server/saved_objects/import/types.ts | 2 +- .../import/validate_object_id.test.ts | 59 ++++++++++ .../import/validate_object_id.ts | 40 +++++++ .../server/saved_objects/routes/import.ts | 4 + .../public/lib/import_file.ts | 6 +- .../objects_table/components/flyout.test.tsx | 1 + .../objects_table/components/flyout.tsx | 10 +- 10 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 src/core/server/saved_objects/import/validate_object_id.test.ts create mode 100644 src/core/server/saved_objects/import/validate_object_id.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8dca630e52..a578087fead9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Add icon in datasource table page to show the default datasource ([#6231](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6231)) - [Multiple Datasource] Add TLS configuration for multiple data sources ([#6171](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6171)) - [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)) 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 a243e08f83e0..2db074119cad 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 575741708f1e..ffefc806a979 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 @@ -231,6 +231,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 586a573ffb53..03b15ada3201 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 @@ -189,13 +189,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 From b9c703bc39c915cb5c240f0930acd9d1a43f4e68 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Tue, 16 Apr 2024 11:26:26 -0700 Subject: [PATCH 2/7] [Multiple Datasource] Add error state to all data source menu components to show error component and consolidate all fetch errors (#6440) * add error state to all components Signed-off-by: Lu Yu * add change log Signed-off-by: Lu Yu * resolve conflict and fix tests Signed-off-by: Lu Yu * move import up Signed-off-by: Lu Yu --------- Signed-off-by: Lu Yu Co-authored-by: ZilongX <99905560+ZilongX@users.noreply.github.com> --- CHANGELOG.md | 1 + .../data_source_aggregated_view.tsx | 20 ++++--- .../data_source_error_menu.tsx | 16 +++++ .../data_source_error_menu/index.ts | 5 ++ .../create_data_source_menu.test.tsx.snap | 60 +++++-------------- .../components/data_source_menu/types.ts | 4 ++ .../data_source_multi_selectable.tsx | 23 ++++--- .../data_source_selectable.test.tsx | 7 ++- .../data_source_selectable.tsx | 26 ++++++-- .../data_source_view.test.tsx.snap | 46 +------------- .../data_source_view/data_source_view.tsx | 23 +++---- .../public/components/utils.test.ts | 4 +- .../public/components/utils.ts | 25 ++++---- 13 files changed, 127 insertions(+), 133 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/data_source_error_menu/data_source_error_menu.tsx create mode 100644 src/plugins/data_source_management/public/components/data_source_error_menu/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a578087fead9..ea2dd672f630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) +- [Multiple Datasource] Add error state to all data source menu components to show error component and consolidate all fetch errors ([#6440](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6440)) - [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)) diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx index 7c039c2f64f3..dda3da78363b 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx @@ -13,9 +13,11 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public'; -import { getDataSourcesWithFields } from '../utils'; +import { getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; +import { DataSourceErrorMenu } from '../data_source_error_menu'; +import { DataSourceBaseState } from '../data_source_menu/types'; interface DataSourceAggregatedViewProps { savedObjectsClient: SavedObjectsClientContract; @@ -27,7 +29,7 @@ interface DataSourceAggregatedViewProps { displayAllCompatibleDataSources: boolean; } -interface DataSourceAggregatedViewState { +interface DataSourceAggregatedViewState extends DataSourceBaseState { isPopoverOpen: boolean; allDataSourcesIdToTitleMap: Map; } @@ -44,6 +46,7 @@ export class DataSourceAggregatedView extends React.Component< this.state = { isPopoverOpen: false, allDataSourcesIdToTitleMap: new Map(), + showError: false, }; } @@ -89,15 +92,18 @@ export class DataSourceAggregatedView extends React.Component< } }) .catch(() => { - this.props.notifications.addWarning( - i18n.translate('dataSource.fetchDataSourceError', { - defaultMessage: 'Unable to fetch existing data sources', - }) - ); + handleDataSourceFetchError(this.onError.bind(this), this.props.notifications); }); } + onError() { + this.setState({ showError: true }); + } + render() { + if (this.state.showError) { + return ; + } const button = ( { + return ( + <> + + Error + + ); +}; diff --git a/src/plugins/data_source_management/public/components/data_source_error_menu/index.ts b/src/plugins/data_source_management/public/components/data_source_error_menu/index.ts new file mode 100644 index 000000000000..1bc0b8eb36e3 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_error_menu/index.ts @@ -0,0 +1,5 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export { DataSourceErrorMenu } from './data_source_error_menu'; diff --git a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap index 31ae3a99d9cd..c520768a6890 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap @@ -5,65 +5,33 @@ Object { "asFragment": [Function], "baseElement":
+
- + Error
, "container":
+
- + Error
, diff --git a/src/plugins/data_source_management/public/components/data_source_menu/types.ts b/src/plugins/data_source_management/public/components/data_source_menu/types.ts index e5f34a3a2979..483f08c524bc 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/types.ts +++ b/src/plugins/data_source_management/public/components/data_source_menu/types.ts @@ -28,6 +28,10 @@ export interface DataSourceBaseConfig { disabled?: boolean; } +export interface DataSourceBaseState { + showError: boolean; +} + export interface DataSourceMenuProps { componentType: DataSourceComponentType; componentConfig: T; diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx index 1a1d958b618c..25122a801e84 100644 --- a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx +++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx @@ -5,10 +5,11 @@ import React from 'react'; import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public'; -import { i18n } from '@osd/i18n'; import { IUiSettingsClient } from 'src/core/public'; import { DataSourceFilterGroup, SelectedDataSourceOption } from './data_source_filter_group'; -import { getDataSourcesWithFields } from '../utils'; +import { getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; +import { DataSourceBaseState } from '../data_source_menu/types'; +import { DataSourceErrorMenu } from '../data_source_error_menu'; export interface DataSourceMultiSeletableProps { savedObjectsClient: SavedObjectsClientContract; @@ -19,7 +20,7 @@ export interface DataSourceMultiSeletableProps { uiSettings?: IUiSettingsClient; } -interface DataSourceMultiSeletableState { +interface DataSourceMultiSeletableState extends DataSourceBaseState { dataSourceOptions: SelectedDataSourceOption[]; selectedOptions: SelectedDataSourceOption[]; defaultDataSource: string | null; @@ -38,6 +39,7 @@ export class DataSourceMultiSelectable extends React.Component< dataSourceOptions: [], selectedOptions: [], defaultDataSource: null, + showError: false, }; } @@ -84,14 +86,18 @@ export class DataSourceMultiSelectable extends React.Component< this.props.onSelectedDataSources(selectedOptions); } catch (error) { - this.props.notifications.addWarning( - i18n.translate('dataSource.fetchDataSourceError', { - defaultMessage: 'Unable to fetch existing data sources', - }) + handleDataSourceFetchError( + this.onError.bind(this), + this.props.notifications, + this.props.onSelectedDataSources ); } } + onError() { + this.setState({ showError: true }); + } + onChange(selectedOptions: SelectedDataSourceOption[]) { if (!this._isMounted) return; this.setState({ @@ -101,6 +107,9 @@ export class DataSourceMultiSelectable extends React.Component< } render() { + if (this.state.showError) { + return ; + } return ( { label: 'test2', }, ], + showError: false, }); containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]); @@ -165,6 +166,7 @@ describe('DataSourceSelectable', () => { label: 'test2', }, ], + showError: false, }); expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]); @@ -341,6 +343,7 @@ describe('DataSourceSelectable', () => { label: 'test2', }, ], + showError: false, }); }); @@ -362,12 +365,13 @@ describe('DataSourceSelectable', () => { const containerInstance = container.instance(); - expect(onSelectedDataSource).toBeCalledTimes(0); + expect(onSelectedDataSource).toBeCalledWith([]); expect(containerInstance.state).toEqual({ dataSourceOptions: [], defaultDataSource: null, isPopoverOpen: false, selectedOption: [], + showError: true, }); containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]); @@ -388,6 +392,7 @@ describe('DataSourceSelectable', () => { label: 'test2', }, ], + showError: true, }); expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]); diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx index cf7c88526065..47e54fae671f 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx @@ -25,11 +25,17 @@ import { getDataSourcesWithFields, getDefaultDataSource, getFilteredDataSources, + handleDataSourceFetchError, } from '../utils'; import { LocalCluster } from '../data_source_selector/data_source_selector'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; -import { DataSourceGroupLabelOption, DataSourceOption } from '../data_source_menu/types'; +import { + DataSourceBaseState, + DataSourceGroupLabelOption, + DataSourceOption, +} from '../data_source_menu/types'; +import { DataSourceErrorMenu } from '../data_source_error_menu'; import { DataSourceItem } from '../data_source_item'; import './data_source_selectable.scss'; import { DataSourceDropDownHeader } from '../drop_down_header'; @@ -47,7 +53,7 @@ interface DataSourceSelectableProps { uiSettings?: IUiSettingsClient; } -interface DataSourceSelectableState { +interface DataSourceSelectableState extends DataSourceBaseState { dataSourceOptions: DataSourceOption[]; isPopoverOpen: boolean; selectedOption?: DataSourceOption[]; @@ -74,6 +80,7 @@ export class DataSourceSelectable extends React.Component< isPopoverOpen: false, selectedOption: [], defaultDataSource: null, + showError: false, }; this.onChange.bind(this); @@ -192,14 +199,18 @@ export class DataSourceSelectable extends React.Component< // handle default data source if there is no valid active option this.handleDefaultDataSource(dataSourceOptions, defaultDataSource); } catch (error) { - this.props.notifications.addWarning( - i18n.translate('dataSource.fetchDataSourceError', { - defaultMessage: 'Unable to fetch existing data sources', - }) + handleDataSourceFetchError( + this.onError.bind(this), + this.props.notifications, + this.props.onSelectedDataSources ); } } + onError() { + this.setState({ showError: true }); + } + onChange(options: DataSourceOption[]) { if (!this._isMounted) return; const optionsWithoutGroupLabel = options.filter( @@ -231,6 +242,9 @@ export class DataSourceSelectable extends React.Component< }; render() { + if (this.state.showError) { + return ; + } const button = ( <> `; -exports[`DataSourceView should call notification warning when there is data source fetch error 1`] = ` - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="dataSourceViewContextMenuPopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > - - - -`; +exports[`DataSourceView should call notification warning when there is data source fetch error 1`] = ``; exports[`DataSourceView should render normally with local cluster not hidden 1`] = ` diff --git a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx index ab48e925d18f..420bb5927145 100644 --- a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx +++ b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx @@ -8,13 +8,14 @@ import { i18n } from '@osd/i18n'; import { EuiPopover, EuiButtonEmpty, EuiButtonIcon, EuiContextMenu } from '@elastic/eui'; import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public'; import { IUiSettingsClient } from 'src/core/public'; -import { DataSourceOption } from '../data_source_menu/types'; +import { DataSourceBaseState, DataSourceOption } from '../data_source_menu/types'; +import { MenuPanelItem } from '../../types'; +import { DataSourceErrorMenu } from '../data_source_error_menu'; import { getDataSourceById, handleDataSourceFetchError, handleNoAvailableDataSourceError, } from '../utils'; -import { MenuPanelItem } from '../../types'; import { LocalCluster } from '../constants'; interface DataSourceViewProps { @@ -28,7 +29,7 @@ interface DataSourceViewProps { onSelectedDataSources?: (dataSources: DataSourceOption[]) => void; } -interface DataSourceViewState { +interface DataSourceViewState extends DataSourceBaseState { selectedOption: DataSourceOption[]; isPopoverOpen: boolean; } @@ -42,6 +43,7 @@ export class DataSourceView extends React.Component; + } const { panels } = this.getPanels(); const button = ( diff --git a/src/plugins/data_source_management/public/components/utils.test.ts b/src/plugins/data_source_management/public/components/utils.test.ts index 397f55333ed2..cc553037188b 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -76,7 +76,9 @@ describe('DataSourceManagement: Utils.ts', () => { const { toasts } = notificationServiceMock.createStartContract(); test('should send warning when data source fetch failed', () => { - handleDataSourceFetchError(toasts); + const changeStateMock = jest.fn(); + handleDataSourceFetchError(changeStateMock, toasts); + expect(changeStateMock).toBeCalledWith({ showError: true }); expect(toasts.addWarning).toHaveBeenCalledWith(`Failed to fetch data source`); }); }); diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 98c23cc140c3..a9f428f5cfa7 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -2,7 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - +import { i18n } from '@osd/i18n'; import { HttpStart, SavedObjectsClientContract, @@ -11,7 +11,6 @@ import { ToastsStart, ApplicationStart, } from 'src/core/public'; -import { i18n } from '@osd/i18n'; import { deepFreeze } from '@osd/std'; import { DataSourceAttributes, @@ -85,14 +84,6 @@ export async function setFirstDataSourceAsDefault( } } -export function handleDataSourceFetchError(notifications: ToastsStart) { - notifications.addWarning( - i18n.translate('dataSource.fetchDataSourceError', { - defaultMessage: `Failed to fetch data source`, - }) - ); -} - export function handleNoAvailableDataSourceError(notifications: ToastsStart) { notifications.addWarning( i18n.translate('dataSource.noAvailableDataSourceError', { @@ -284,6 +275,20 @@ export const extractRegisteredAuthTypeCredentials = ( return registeredCredentials; }; +export const handleDataSourceFetchError = ( + changeState: (state: { showError: boolean }) => void, + notifications: ToastsStart, + callback?: (ds: DataSourceOption[]) => void +) => { + changeState({ showError: true }); + if (callback) callback([]); + notifications.addWarning( + i18n.translate('dataSource.fetchDataSourceError', { + defaultMessage: 'Failed to fetch data source', + }) + ); +}; + interface DataSourceOptionGroupLabel { [key: string]: DataSourceGroupLabelOption; } From be0f9d555cab325736ac55dead9ea9f8c6fa00a7 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:57:05 -0700 Subject: [PATCH 3/7] [MDS] TSVB Support (#6298) * Add MDS support to TSVB Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactor datasource picker component Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Allow picker to persist state Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactored picker component params Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Add unit tests Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Add to CHANGELOG Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactor components to use hideLocalCluster Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Remove Picker wrapper Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Update selector component and rename field to index name Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Address comments Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactor to use different decideClient Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Add optional arg Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Remove hidelocalcluster as a setting Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Fixed case where local cluster is disabled but the datasource id could be local cluster Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Add test for create data source picker handler Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --------- Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- CHANGELOG.md | 1 + src/plugins/data/server/index.ts | 1 + .../data/server/index_patterns/index.ts | 1 + .../data/server/index_patterns/routes.ts | 2 +- .../data_source_selector.tsx | 2 + .../components/data_source_selector/index.ts | 2 +- .../data_source_management/public/index.ts | 2 +- .../vis_type_timeseries/common/constants.ts | 1 + .../vis_type_timeseries/common/vis_schema.ts | 1 + .../opensearch_dashboards.json | 2 +- .../components/annotations_editor.js | 7 ++- .../application/components/index_pattern.js | 53 +++++++++++++++++-- .../create_data_source_change_handler.test.ts | 49 +++++++++++++++++ .../lib/create_data_source_change_handler.ts | 16 ++++++ .../application/components/vis_editor.js | 9 +++- .../public/application/lib/fetch_fields.js | 7 ++- .../vis_type_timeseries/public/plugin.ts | 16 +++++- .../vis_type_timeseries/public/services.ts | 17 +++++- .../server/lib/get_fields.ts | 7 +-- .../abstract_search_strategy.test.js | 48 ++++++++++++++++- .../strategies/abstract_search_strategy.ts | 3 +- .../server/lib/vis_data/get_annotations.js | 3 +- .../server/lib/vis_data/get_series_data.js | 3 +- .../server/lib/vis_data/get_table_data.js | 18 ++++--- .../vis_type_timeseries/server/plugin.ts | 2 + .../server/routes/fields.ts | 9 +++- 26 files changed, 251 insertions(+), 31 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.test.ts create mode 100644 src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2dd672f630..c409485dee50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) +- [Mulitple Datasource] Add multi data source support to TSVB ([#6298](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6298)) - [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)) diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 46c2b1ca0477..4bc3ad62a4ae 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -145,6 +145,7 @@ export { FieldDescriptor as IndexPatternFieldDescriptor, shouldReadFieldFromDocValues, // used only in logstash_fields fixture FieldDescriptor, + decideClient, } from './index_patterns'; export { diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index b2e832294e41..771aa9c09ab8 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -31,3 +31,4 @@ export * from './utils'; export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues } from './fetcher'; export { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns_service'; +export { decideClient } from './routes'; diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 3adc1970dd81..8b3c7139ffc0 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -155,7 +155,7 @@ export function registerRoutes(http: HttpServiceSetup) { ); } -const decideClient = async ( +export const decideClient = async ( context: RequestHandlerContext, request: any ): Promise => { diff --git a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx index bfe02b7288a2..8d2a90943b9f 100644 --- a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx @@ -34,6 +34,7 @@ export interface DataSourceSelectorProps { dataSourceFilter?: (dataSource: SavedObject) => boolean; compressed?: boolean; uiSettings?: IUiSettingsClient; + isClearable?: boolean; } interface DataSourceSelectorState { @@ -202,6 +203,7 @@ export class DataSourceSelector extends React.Component< return ( } + helpText={i18n.translate('visTypeTimeseries.indexPattern.searchByIndex', { + defaultMessage: + 'Use an asterisk (*) to match multiple indices. Spaces and the characters , /, ?, ", <, >, | are not allowed.', + })} fullWidth > )} + {!!dataSourceManagementEnabled && ( + + + + + + + + )} , | are not allowed.', + }) } > { + let handleChange: jest.Mock; + let changeHandler: (selectedOptions: []) => void; + + beforeEach(() => { + handleChange = jest.fn(); + changeHandler = createDataSourcePickerHandler(handleChange); + }); + + test.each([ + { + id: undefined, + }, + {}, + ])( + 'calls handleChange() and sets data_source_id to undefined if id cannot be found or is undefined', + ({ id }) => { + // @ts-ignore + changeHandler([{ id }]); + expect(handleChange.mock.calls.length).toEqual(1); + expect(handleChange.mock.calls[0][0]).toEqual({ + data_source_id: undefined, + }); + } + ); + + test.each([ + { + id: '', + }, + { + id: 'foo', + }, + ])('calls handleChange() function with partial and updates the data_source_id', ({ id }) => { + // @ts-ignore + changeHandler([{ id }]); + expect(handleChange.mock.calls.length).toEqual(1); + expect(handleChange.mock.calls[0][0]).toEqual({ + data_source_id: id, + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.ts new file mode 100644 index 000000000000..5fa18d74c5b3 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import _ from 'lodash'; + +import { PanelSchema } from 'src/plugins/vis_type_timeseries/common/types'; +import { DATA_SOURCE_ID_KEY } from '../../../../common/constants'; + +export const createDataSourcePickerHandler = (handleChange: (e: PanelSchema) => void) => { + return (selectedOptions: []): void => { + return handleChange?.({ + [DATA_SOURCE_ID_KEY]: _.get(selectedOptions, '[0].id', undefined), + } as PanelSchema); + }; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index e3d78bd0a824..c91d1f084f91 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -41,6 +41,7 @@ import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; +import { DATA_SOURCE_ID_KEY } from '../../../common/constants'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/opensearch_dashboards_react/public'; @@ -113,9 +114,13 @@ export class VisEditor extends Component { } if (this.props.isEditorMode) { + const dataSourceId = nextModel[DATA_SOURCE_ID_KEY] || undefined; const extractedIndexPatterns = extractIndexPatterns(nextModel); - if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) { - fetchFields(extractedIndexPatterns).then((visFields) => + if ( + !isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns) || + !isEqual(this.state.model[DATA_SOURCE_ID_KEY], dataSourceId) + ) { + fetchFields(extractedIndexPatterns, dataSourceId).then((visFields) => this.setState({ visFields, extractedIndexPatterns, diff --git a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js index cac4c910fee4..8aa9bb618cad 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js @@ -30,9 +30,10 @@ import { i18n } from '@osd/i18n'; import { extractIndexPatterns } from '../../../common/extract_index_patterns'; +import { DATA_SOURCE_ID_KEY } from '../../../common/constants'; import { getCoreStart } from '../../services'; -export async function fetchFields(indexPatterns = ['*']) { +export async function fetchFields(indexPatterns = ['*'], dataSourceId = undefined) { const patterns = Array.isArray(indexPatterns) ? indexPatterns : [indexPatterns]; try { const indexFields = await Promise.all( @@ -40,6 +41,7 @@ export async function fetchFields(indexPatterns = ['*']) { return getCoreStart().http.get('/api/metrics/fields', { query: { index: pattern, + data_source: dataSourceId, }, }); }) @@ -62,7 +64,8 @@ export async function fetchFields(indexPatterns = ['*']) { } export async function fetchIndexPatternFields({ params, fields = {} }) { + const dataSourceId = params[DATA_SOURCE_ID_KEY] || undefined; const indexPatterns = extractIndexPatterns(params, fields); - return await fetchFields(indexPatterns); + return await fetchFields(indexPatterns, dataSourceId); } diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index da565a160164..0220a5320422 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -36,6 +36,8 @@ import { CoreStart, Plugin, } from 'opensearch-dashboards/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; +import { DataSourcePluginSetup } from 'src/plugins/data_source/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; @@ -49,6 +51,8 @@ import { setCoreStart, setDataStart, setChartsSetup, + setDataSourceManagementSetup, + setNotifications, } from './services'; import { DataPublicPluginStart } from '../../data/public'; import { ChartsPluginSetup } from '../../charts/public'; @@ -58,6 +62,8 @@ export interface MetricsPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + dataSourceManagement?: DataSourceManagementPluginSetup; + dataSource?: DataSourcePluginSetup; } /** @internal */ @@ -75,12 +81,19 @@ export class MetricsPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, charts }: MetricsPluginSetupDependencies + { + expressions, + visualizations, + charts, + dataSourceManagement, + dataSource, + }: MetricsPluginSetupDependencies ) { expressions.registerFunction(createMetricsFn); setUISettings(core.uiSettings); setChartsSetup(charts); visualizations.createReactVisualization(metricsVisDefinition); + setDataSourceManagementSetup({ dataSourceManagement }); } public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { @@ -89,5 +102,6 @@ export class MetricsPlugin implements Plugin, void> { setFieldFormats(data.fieldFormats); setDataStart(data); setCoreStart(core); + setNotifications(core.notifications); } } diff --git a/src/plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_type_timeseries/public/services.ts index 15532bc4fd6f..5f54ac3e7546 100644 --- a/src/plugins/vis_type_timeseries/public/services.ts +++ b/src/plugins/vis_type_timeseries/public/services.ts @@ -28,7 +28,14 @@ * under the License. */ -import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; +import { + I18nStart, + SavedObjectsStart, + IUiSettingsClient, + CoreStart, + NotificationsStart, +} from 'src/core/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; @@ -52,3 +59,11 @@ export const [getI18n, setI18n] = createGetterSetter('I18n'); export const [getChartsSetup, setChartsSetup] = createGetterSetter( 'ChartsPluginSetup' ); + +export const [getDataSourceManagementSetup, setDataSourceManagementSetup] = createGetterSetter<{ + dataSourceManagement: DataSourceManagementPluginSetup | undefined; +}>('DataSourceManagementSetup'); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 1752d3f91f86..56a58b43b45e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -37,6 +37,7 @@ import { indexPatterns, IndexPatternFieldDescriptor, IndexPatternsFetcher, + decideClient, } from '../../../data/server'; import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy'; @@ -50,15 +51,15 @@ export async function getFields( // removes the need to refactor many layers of dependencies on "req", and instead just augments the top // level object passed from here. The layers should be refactored fully at some point, but for now // this works and we are still using the New Platform services for these vis data portions. + const client = await decideClient(requestContext, request); + const reqFacade: ReqFacade = { requestContext, ...request, framework, payload: {}, pre: { - indexPatternsService: new IndexPatternsFetcher( - requestContext.core.opensearch.legacy.client.callAsCurrentUser - ), + indexPatternsService: new IndexPatternsFetcher(client), }, getUiSettingsService: () => requestContext.core.uiSettings.client, getSavedObjectsClient: () => requestContext.core.savedObjects.client, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index fa130462e78d..068a7ef06c04 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -65,7 +65,7 @@ describe('AbstractSearchStrategy', () => { }); }); - test('should return response', async () => { + test('should return response for local cluster queries', async () => { const searches = [{ body: 'body', index: 'index' }]; const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); @@ -107,4 +107,50 @@ describe('AbstractSearchStrategy', () => { } ); }); + + test('should return response for datasource query', async () => { + const searches = [{ body: 'body', index: 'index' }]; + const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); + + const responses = await abstractSearchStrategy.search( + { + requestContext: {}, + framework: { + core: { + getStartServices: jest.fn().mockReturnValue( + Promise.resolve([ + {}, + { + data: { + search: { + search: searchFn, + }, + }, + }, + ]) + ), + }, + }, + }, + searches, + {}, + 'some-data-source-id' + ); + + expect(responses).toEqual([{}]); + expect(searchFn).toHaveBeenCalledWith( + {}, + { + dataSourceId: 'some-data-source-id', + params: { + body: 'body', + index: 'index', + }, + indexType: undefined, + }, + { + strategy: 'opensearch', + } + ); + }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 6b69628353ac..7c333fad7f73 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -66,7 +66,7 @@ export class AbstractSearchStrategy { this.additionalParams = additionalParams; } - async search(req: ReqFacade, bodies: any[], options = {}) { + async search(req: ReqFacade, bodies: any[], options = {}, dataSourceId?: string) { const [, deps] = await req.framework.core.getStartServices(); const requests: any[] = []; bodies.forEach((body) => { @@ -74,6 +74,7 @@ export class AbstractSearchStrategy { deps.data.search.search( req.requestContext, { + ...(!!dataSourceId && { dataSourceId }), params: { ...body, ...this.additionalParams, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js index 16b526d1ba2e..3dd88b830346 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js @@ -54,6 +54,7 @@ export async function getAnnotations({ const annotations = panel.annotations.filter(validAnnotation); const lastSeriesTimestamp = getLastSeriesTimestamp(series); const handleAnnotationResponseBy = handleAnnotationResponse(lastSeriesTimestamp); + const panelDataSourceId = panel.data_source_id; const bodiesPromises = annotations.map((annotation) => getAnnotationRequestParams(req, panel, annotation, opensearchQueryConfig, capabilities) @@ -67,7 +68,7 @@ export async function getAnnotations({ if (!searches.length) return { responses: [] }; try { - const data = await searchStrategy.search(req, searches); + const data = await searchStrategy.search(req, searches, {}, panelDataSourceId); return annotations.reduce((acc, annotation, index) => { acc[annotation.id] = handleAnnotationResponseBy(data[index].rawResponse, annotation); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js index a5c8a239d2b0..59d445f93324 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js @@ -41,6 +41,7 @@ export async function getSeriesData(req, panel) { capabilities, } = await req.framework.searchStrategyRegistry.getViableStrategyForPanel(req, panel); const opensearchQueryConfig = await getOpenSearchQueryConfig(req); + const panelDataSourceId = panel.data_source_id; const meta = { type: panel.type, uiRestrictions: capabilities.uiRestrictions, @@ -56,7 +57,7 @@ export async function getSeriesData(req, panel) { [] ); - const data = await searchStrategy.search(req, searches); + const data = await searchStrategy.search(req, searches, {}, panelDataSourceId); const handleResponseBodyFn = handleResponseBody(panel); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js index 0b744638c3d8..20dfa06c1a78 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js @@ -37,6 +37,7 @@ import { getIndexPatternObject } from './helpers/get_index_pattern'; export async function getTableData(req, panel) { const panelIndexPattern = panel.index_pattern; + const panelDataSourceId = panel.data_source_id; const { searchStrategy, @@ -58,12 +59,17 @@ export async function getTableData(req, panel) { indexPatternObject, capabilities ); - const [resp] = await searchStrategy.search(req, [ - { - body, - index: panelIndexPattern, - }, - ]); + const [resp] = await searchStrategy.search( + req, + [ + { + body, + index: panelIndexPattern, + }, + ], + {}, + panelDataSourceId + ); const buckets = get( resp.rawResponse ? resp.rawResponse : resp, diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index 654c85d452b1..af95e2e6b5dc 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -40,6 +40,7 @@ import { } from 'src/core/server'; import { Observable } from 'rxjs'; import { Server } from '@hapi/hapi'; +import { DataSourcePluginSetup } from 'src/plugins/data_source/server'; import { VisTypeTimeseriesConfig } from './config'; import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data'; import { ValidationTelemetryService } from './validation_telemetry'; @@ -57,6 +58,7 @@ export interface LegacySetup { interface VisTypeTimeseriesPluginSetupDependencies { usageCollection?: UsageCollectionSetup; + dataSource?: DataSourcePluginSetup; } interface VisTypeTimeseriesPluginStartDependencies { diff --git a/src/plugins/vis_type_timeseries/server/routes/fields.ts b/src/plugins/vis_type_timeseries/server/routes/fields.ts index bff34ee159f5..80a04918c517 100644 --- a/src/plugins/vis_type_timeseries/server/routes/fields.ts +++ b/src/plugins/vis_type_timeseries/server/routes/fields.ts @@ -38,12 +38,17 @@ export const fieldsRoutes = (framework: Framework) => { { path: '/api/metrics/fields', validate: { - query: schema.object({ index: schema.string() }), + query: schema.object({ + index: schema.string(), + data_source: schema.maybe(schema.string()), + }), }, }, async (context, req, res) => { try { - return res.ok({ body: await getFields(context, req, framework, req.query.index) }); + return res.ok({ + body: await getFields(context, req, framework, req.query.index), + }); } catch (err) { if (isBoom(err) && err.output.statusCode === 401) { return res.customError({ From 4199162bbdf8245d72f9e9d809504c2055c2d39e Mon Sep 17 00:00:00 2001 From: Samuel Valdes Gutierrez Date: Tue, 16 Apr 2024 15:58:37 -0400 Subject: [PATCH 4/7] [OSCI][FEAT] Changelog Project - PoC Changelog and release notes automation tool - OpenSearch Dashboards (#5519) Refactor and Enhance Workflow Management - Added and updated changesets for multiple PRs to improve tracking and documentation of changes. - Removed unnecessary test and dummy files (`test.txt`, various `.yml` fragments) to clean up the repository. - Refactored workflow scripts to streamline changelog generation and fragment handling, moving temporary files to a designated folder. - Updated GitHub Actions workflows by changing event triggers from `pull_request` to `pull_request_target` and vice versa to optimize workflow execution. - Enhanced security and automation by updating token names and adding write permissions to the changeset workflow. - Deleted obsolete workflow file for creating changeset files, now handled by an automated process. - Major clean-up of dummy fragment files and unnecessary changelog entries to maintain clarity and relevancy in documentation. - Implemented minor updates and improvements in codebase, specifically in generating release notes and handling fragments. --------- Signed-off-by: Johnathon Bowers Signed-off-by: CMDWillYang Signed-off-by: Qiwen Li Signed-off-by: qiwen li Signed-off-by: Samuel Valdes Gutierrez Signed-off-by: Ashwin P Chandran Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Johnathon Bowers Co-authored-by: CMDWillYang Co-authored-by: Qiwen Li Co-authored-by: Ashwin P Chandran Co-authored-by: Anan Zhuang Co-authored-by: Josh Romero Co-authored-by: autochangeset[bot] <154024398+autochangeset[bot]@users.noreply.github.com> Co-authored-by: opensearch-bot[bot] <154024398+opensearch-bot[bot]@users.noreply.github.com> Co-authored-by: opensearch-bot-dev[bot] <154634848+opensearch-bot-dev[bot]@users.noreply.github.com> Co-authored-by: Ashwin P Chandran Co-authored-by: Miki --- .github/pull_request_template.md | 12 ++ .github/workflows/changelog_verifier.yml | 19 --- .../opensearch_changelog_workflow.yml | 23 +++ changelogs/README.md | 5 + package.json | 1 + scripts/generate_release_note.js | 8 ++ src/dev/generate_release_note.ts | 134 ++++++++++++++++++ src/dev/generate_release_note_helper.ts | 59 ++++++++ 8 files changed, 242 insertions(+), 19 deletions(-) delete mode 100644 .github/workflows/changelog_verifier.yml create mode 100644 .github/workflows/opensearch_changelog_workflow.yml create mode 100644 changelogs/README.md create mode 100644 scripts/generate_release_note.js create mode 100644 src/dev/generate_release_note.ts create mode 100644 src/dev/generate_release_note_helper.ts diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3cb6f172b119..662ca15b8d68 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,6 +19,18 @@ the functionality of your change --> +## Changelog + + ### Check List - [ ] All tests pass diff --git a/.github/workflows/changelog_verifier.yml b/.github/workflows/changelog_verifier.yml deleted file mode 100644 index 0890ea8b8fbb..000000000000 --- a/.github/workflows/changelog_verifier.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: "Changelog Verifier" -on: - pull_request: - branches: [ '**', '!feature/**' ] - types: [opened, edited, review_requested, synchronize, reopened, ready_for_review, labeled, unlabeled] - -jobs: - # Enforces the update of a changelog file on every pull request - verify-changelog: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ github.event.pull_request.head.sha }} - - - uses: dangoslen/changelog-enforcer@v3 - with: - skipLabels: "autocut, Skip-Changelog" diff --git a/.github/workflows/opensearch_changelog_workflow.yml b/.github/workflows/opensearch_changelog_workflow.yml new file mode 100644 index 000000000000..8af8b0d70b0d --- /dev/null +++ b/.github/workflows/opensearch_changelog_workflow.yml @@ -0,0 +1,23 @@ +name: OpenSearch Changelog Workflow + +on: + pull_request_target: + types: [opened, reopened, edited] + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + update-changelog: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Parse changelog entries and submit request for changset creation + uses: BigSamu/OpenSearch_Changelog_Workflow@1.0.0-alpha1 + with: + token: ${{secrets.GITHUB_TOKEN}} + CHANGELOG_PR_BRIDGE_URL_DOMAIN: ${{secrets.CHANGELOG_PR_BRIDGE_URL_DOMAIN}} + CHANGELOG_PR_BRIDGE_API_KEY: ${{secrets.CHANGELOG_PR_BRIDGE_API_KEY}} diff --git a/changelogs/README.md b/changelogs/README.md new file mode 100644 index 000000000000..a4620754cfd1 --- /dev/null +++ b/changelogs/README.md @@ -0,0 +1,5 @@ +# Changelog and Release Notes + +For information regarding the changelog and release notes process, please consult the README in the GitHub Actions repository that this process utilizes. To view this README, follow the link below: + +[GitHub Actions Workflow README](https://github.com/BigSamu/OpenSearch_Change_Set_Create_Action/blob/main/README.md) diff --git a/package.json b/package.json index 172c48cfbd70..c728618fcbcd 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "osd:bootstrap": "scripts/use_node scripts/build_ts_refs && scripts/use_node scripts/register_git_hook", "spec_to_console": "scripts/use_node scripts/spec_to_console", "pkg-version": "scripts/use_node -e \"console.log(require('./package.json').version)\"", + "release_note:generate": "scripts/use_node scripts/generate_release_note", "cypress:run-without-security": "env TZ=America/Los_Angeles NO_COLOR=1 cypress run --headless --env SECURITY_ENABLED=false", "cypress:run-with-security": "env TZ=America/Los_Angeles NO_COLOR=1 cypress run --headless --env SECURITY_ENABLED=true,openSearchUrl=https://localhost:9200,WAIT_FOR_LOADER_BUFFER_MS=500", "osd:ciGroup10": "echo \"dashboard_sanity_test_spec.js\"", diff --git a/scripts/generate_release_note.js b/scripts/generate_release_note.js new file mode 100644 index 000000000000..4721fe0dec35 --- /dev/null +++ b/scripts/generate_release_note.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +require('../src/setup_node_env'); +require('../src/dev/generate_release_note'); +require('../src/dev/generate_release_note_helper'); diff --git a/src/dev/generate_release_note.ts b/src/dev/generate_release_note.ts new file mode 100644 index 000000000000..4c9eaabf0bf7 --- /dev/null +++ b/src/dev/generate_release_note.ts @@ -0,0 +1,134 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { join, resolve } from 'path'; +import { readFileSync, writeFileSync, Dirent, rm, rename, promises as fsPromises } from 'fs'; +import { load as loadYaml } from 'js-yaml'; +import { readdir } from 'fs/promises'; +import { version as pkgVersion } from '../../package.json'; +import { + validateFragment, + getCurrentDateFormatted, + Changelog, + SECTION_MAPPING, + fragmentDirPath, + SectionKey, + releaseNotesDirPath, + filePath, +} from './generate_release_note_helper'; + +// Function to add content after the 'Unreleased' section in the changelog +function addContentAfterUnreleased(path: string, newContent: string): void { + let fileContent = readFileSync(path, 'utf8'); + const targetString = '## [Unreleased]'; + const targetIndex = fileContent.indexOf(targetString); + + if (targetIndex !== -1) { + const endOfLineIndex = fileContent.indexOf('\n', targetIndex); + if (endOfLineIndex !== -1) { + fileContent = + fileContent.slice(0, endOfLineIndex + 1) + + '\n' + + newContent + + '\n' + + fileContent.slice(endOfLineIndex + 1); + } else { + throw new Error('End of line for "Unreleased" section not found.'); + } + } else { + throw new Error("'## [Unreleased]' not found in the file."); + } + + writeFileSync(path, fileContent); +} + +async function deleteFragments(fragmentTempDirPath: string) { + rm(fragmentTempDirPath, { recursive: true }, (err: any) => { + if (err) { + throw err; + } + }); +} + +// Read fragment files and populate sections +async function readFragments() { + // Initialize sections + const sections: Changelog = (Object.fromEntries( + Object.keys(SECTION_MAPPING).map((key) => [key, []]) + ) as unknown) as Changelog; + + const fragmentPaths = await readdir(fragmentDirPath, { withFileTypes: true }); + for (const fragmentFilename of fragmentPaths) { + // skip non yml or yaml files + if (!/\.ya?ml$/i.test(fragmentFilename.name)) { + // eslint-disable-next-line no-console + console.warn(`Skipping non yml or yaml file ${fragmentFilename.name}`); + continue; + } + + const fragmentPath = join(fragmentDirPath, fragmentFilename.name); + const fragmentContents = readFileSync(fragmentPath, { encoding: 'utf-8' }); + + validateFragment(fragmentContents); + + const fragmentYaml = loadYaml(fragmentContents) as Changelog; + + for (const [sectionKey, entries] of Object.entries(fragmentYaml)) { + sections[sectionKey as SectionKey].push(...entries); + } + } + return { sections, fragmentPaths }; +} + +async function moveFragments(fragmentPaths: Dirent[], fragmentTempDirPath: string): Promise { + // Move fragment files to temp fragments folder + for (const fragmentFilename of fragmentPaths) { + const fragmentPath = resolve(fragmentDirPath, fragmentFilename.name); + const fragmentTempPath = resolve(fragmentTempDirPath, fragmentFilename.name); + rename(fragmentPath, fragmentTempPath, () => {}); + } +} + +function generateChangelog(sections: Changelog) { + // Generate changelog sections + const changelogSections = Object.entries(sections).map(([sectionKey, entries]) => { + const sectionName = SECTION_MAPPING[sectionKey as SectionKey]; + return entries.length === 0 + ? `### ${sectionName}` + : `### ${sectionName}\n\n${entries.map((entry) => ` - ${entry}`).join('\n')}`; + }); + + // Generate full changelog + const currentDate = getCurrentDateFormatted(); + const changelog = `## [${pkgVersion}-${currentDate}](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/tag/${pkgVersion})\n\n${changelogSections.join( + '\n\n' + )}`; + // Update changelog file + addContentAfterUnreleased(filePath, changelog); + return changelogSections; +} + +function generateReleaseNote(changelogSections: string[]) { + // Generate release note + const releaseNoteFilename = `opensearch-dashboards.release-notes-${pkgVersion}.md`; + const releaseNoteHeader = `# VERSION ${pkgVersion} Release Note`; + const releaseNote = `${releaseNoteHeader}\n\n${changelogSections.join('\n\n')}`; + writeFileSync(resolve(releaseNotesDirPath, releaseNoteFilename), releaseNote); +} + +(async () => { + const { sections, fragmentPaths } = await readFragments(); + // create folder for temp fragments + const fragmentTempDirPath = await fsPromises.mkdtemp(join(fragmentDirPath, 'tmp_fragments-')); + // move fragments to temp fragments folder + await moveFragments(fragmentPaths, fragmentTempDirPath); + + const changelogSections = generateChangelog(sections); + + generateReleaseNote(changelogSections); + + // remove temp fragments folder + await deleteFragments(fragmentTempDirPath); +})(); diff --git a/src/dev/generate_release_note_helper.ts b/src/dev/generate_release_note_helper.ts new file mode 100644 index 000000000000..988c0f92d964 --- /dev/null +++ b/src/dev/generate_release_note_helper.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { resolve } from 'path'; + +export const filePath = resolve(__dirname, '..', '..', 'CHANGELOG.md'); +export const fragmentDirPath = resolve(__dirname, '..', '..', 'changelogs', 'fragments'); +export const releaseNotesDirPath = resolve(__dirname, '..', '..', 'release-notes'); + +export function getCurrentDateFormatted(): string { + return new Date().toISOString().slice(0, 10); +} + +export const SECTION_MAPPING = { + breaking: '💥 Breaking Changes', + deprecate: 'Deprecations', + security: '🛡 Security', + feat: '📈 Features/Enhancements', + fix: '🐛 Bug Fixes', + infra: '🚞 Infrastructure', + doc: '📝 Documentation', + chore: '🛠 Maintenance', + refactor: '🪛 Refactoring', + test: '🔩 Tests', +}; + +export type SectionKey = keyof typeof SECTION_MAPPING; +export type Changelog = Record; + +const MAX_ENTRY_LENGTH = 100; +// Each entry must start with '-' and a space, followed by a non-empty string, and be no longer that MAX_ENTRY_LENGTH characters +const entryRegex = new RegExp(`^-.{1,${MAX_ENTRY_LENGTH}}\\(\\[#.+]\\(.+\\)\\)$`); + +// validate format of fragment files +export function validateFragment(content: string) { + const sections = content.split(/(?:\r?\n){2,}/); + + // validate each section + for (const section of sections) { + const lines = section.split('\n'); + const sectionName = lines[0]; + const sectionKey = sectionName.slice(0, -1); + + if (!SECTION_MAPPING[sectionKey as SectionKey] || !sectionName.endsWith(':')) { + throw new Error(`Unknown section ${sectionKey}.`); + } + for (const entry of lines.slice(1)) { + if (entry === '') { + continue; + } + // if (!entryRegex.test(entry)) { + if (!entryRegex.test(entry.trim())) { + throw new Error(`Invalid entry ${entry} in section ${sectionKey}.`); + } + } + } +} From 73e5d78c968f1ab20d28eed6ac6ea772f9c6b620 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:59:58 -0700 Subject: [PATCH 5/7] [MDS] Support for Timeline (#6385) * Add MDS support to Timeline Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactor to function and add unit tests Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Fix typo in args Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactor build request to pass unit tests Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Add to CHANGELOG Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactor error messages + address comments Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Fix ut Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Change to data source feature Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Fix UT Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Address comments Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --------- Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- CHANGELOG.md | 1 + .../opensearch_dashboards.json | 1 + .../helpers/timeline_request_handler.ts | 4 +- .../server/handlers/chain_runner.js | 4 +- .../server/lib/fetch_data_source_id.test.ts | 152 ++++++++++++++++++ .../server/lib/fetch_data_source_id.ts | 42 +++++ .../vis_type_timeline/server/lib/services.ts | 10 ++ .../vis_type_timeline/server/plugin.ts | 10 +- .../vis_type_timeline/server/routes/run.ts | 5 +- .../series_functions/opensearch/index.js | 19 ++- .../opensearch/lib/build_request.js | 3 +- src/plugins/vis_type_timeline/server/types.ts | 15 ++ 12 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.test.ts create mode 100644 src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.ts create mode 100644 src/plugins/vis_type_timeline/server/lib/services.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c409485dee50..9cf4d4b0570d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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] Add multi data source support to Timeline ([#6385](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6385)) - [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)) diff --git a/src/plugins/vis_type_timeline/opensearch_dashboards.json b/src/plugins/vis_type_timeline/opensearch_dashboards.json index 14b3af415177..c4fa1e8d40fd 100644 --- a/src/plugins/vis_type_timeline/opensearch_dashboards.json +++ b/src/plugins/vis_type_timeline/opensearch_dashboards.json @@ -4,6 +4,7 @@ "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, + "optionalPlugins": ["dataSource"], "requiredPlugins": ["visualizations", "data", "expressions"], "requiredBundles": ["opensearchDashboardsUtils", "opensearchDashboardsReact", "visDefaultEditor"] } diff --git a/src/plugins/vis_type_timeline/public/helpers/timeline_request_handler.ts b/src/plugins/vis_type_timeline/public/helpers/timeline_request_handler.ts index 467fef727f29..d7b955f96ef9 100644 --- a/src/plugins/vis_type_timeline/public/helpers/timeline_request_handler.ts +++ b/src/plugins/vis_type_timeline/public/helpers/timeline_request_handler.ts @@ -129,10 +129,12 @@ export function getTimelineRequestHandler({ }); } catch (e) { if (e && e.body) { + const errorTitle = + e.body.attributes && e.body.attributes.title ? ` (${e.body.attributes.title})` : ''; const err = new Error( `${i18n.translate('timeline.requestHandlerErrorTitle', { defaultMessage: 'Timeline request error', - })}: ${e.body.title} ${e.body.message}` + })}${errorTitle}: ${e.body.message}` ); err.stack = e.stack; throw err; diff --git a/src/plugins/vis_type_timeline/server/handlers/chain_runner.js b/src/plugins/vis_type_timeline/server/handlers/chain_runner.js index 75382b73de57..39af9939056f 100644 --- a/src/plugins/vis_type_timeline/server/handlers/chain_runner.js +++ b/src/plugins/vis_type_timeline/server/handlers/chain_runner.js @@ -47,7 +47,9 @@ export default function chainRunner(tlConfig) { let sheet; function throwWithCell(cell, exception) { - throw new Error(' in cell #' + (cell + 1) + ': ' + exception.message); + const e = new Error(exception.message); + e.name = `Expression parse error in cell #${cell + 1}`; + throw e; } // Invokes a modifier function, resolving arguments into series as needed diff --git a/src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.test.ts b/src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.test.ts new file mode 100644 index 000000000000..e5596a001a2d --- /dev/null +++ b/src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { savedObjectsClientMock } from '../../../../core/server/mocks'; +import { fetchDataSourceIdByName } from './fetch_data_source_id'; +import { OpenSearchFunctionConfig } from '../types'; + +jest.mock('./services', () => ({ + getDataSourceEnabled: jest + .fn() + .mockReturnValueOnce({ enabled: false }) + .mockReturnValue({ enabled: true }), +})); + +describe('fetchDataSourceIdByName()', () => { + const validId = 'some-valid-id'; + const config: OpenSearchFunctionConfig = { + q: null, + metric: null, + split: null, + index: null, + timefield: null, + kibana: null, + opensearchDashboards: null, + interval: null, + }; + const client = savedObjectsClientMock.create(); + client.find = jest.fn().mockImplementation((props) => { + if (props.search === '"No Results With Filter"') { + return Promise.resolve({ + saved_objects: [ + { + id: 'some-non-matching-id', + attributes: { + title: 'No Results With Filter Some Suffix', + }, + }, + ], + }); + } + if (props.search === '"Duplicate Title"') { + return Promise.resolve({ + saved_objects: [ + { + id: 'duplicate-id-1', + attributes: { + title: 'Duplicate Title', + }, + }, + { + id: 'duplicate-id-2', + attributes: { + title: 'Duplicate Title', + }, + }, + ], + }); + } + if (props.search === '"Some Data Source"') { + return Promise.resolve({ + saved_objects: [ + { + id: validId, + attributes: { + title: 'Some Data Source', + }, + }, + ], + }); + } + if (props.search === '"Some Prefix"') { + return Promise.resolve({ + saved_objects: [ + { + id: 'some-id-2', + attributes: { + title: 'Some Prefix B', + }, + }, + { + id: validId, + attributes: { + title: 'Some Prefix', + }, + }, + ], + }); + } + + return Promise.resolve({ saved_objects: [] }); + }); + + it('should return undefined if data_source_name is not present', async () => { + expect(await fetchDataSourceIdByName(config, client)).toBe(undefined); + }); + + it('should return undefined if data_source_name is an empty string', async () => { + expect(await fetchDataSourceIdByName({ ...config, data_source_name: '' }, client)).toBe( + undefined + ); + }); + + it('should throw errors when MDS is disabled', async () => { + await expect( + fetchDataSourceIdByName({ ...config, data_source_name: 'Some Data Source' }, client) + ).rejects.toThrowError( + 'To query from multiple data sources, first enable the data source feature' + ); + }); + + it.each([ + { + dataSourceName: 'Non-existent Data Source', + expectedResultCount: 0, + }, + { + dataSourceName: 'No Results With Filter', + expectedResultCount: 0, + }, + { + dataSourceName: 'Duplicate Title', + expectedResultCount: 2, + }, + ])( + 'should throw errors when non-existent or duplicate data_source_name is provided', + async ({ dataSourceName, expectedResultCount }) => { + await expect( + fetchDataSourceIdByName({ ...config, data_source_name: dataSourceName }, client) + ).rejects.toThrowError( + `Expected exactly 1 result for data_source_name "${dataSourceName}" but got ${expectedResultCount} results` + ); + } + ); + + it.each([ + { + dataSourceName: 'Some Data Source', + }, + { + dataSourceName: 'Some Prefix', + }, + ])( + 'should return valid id when data_source_name exists and is unique', + async ({ dataSourceName }) => { + expect( + await fetchDataSourceIdByName({ ...config, data_source_name: dataSourceName }, client) + ).toBe(validId); + } + ); +}); diff --git a/src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.ts b/src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.ts new file mode 100644 index 000000000000..e3d0d76d23e7 --- /dev/null +++ b/src/plugins/vis_type_timeline/server/lib/fetch_data_source_id.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources'; +import { getDataSourceEnabled } from './services'; +import { OpenSearchFunctionConfig } from '../types'; + +export const fetchDataSourceIdByName = async ( + config: OpenSearchFunctionConfig, + client: SavedObjectsClientContract +) => { + if (!config.data_source_name) { + return undefined; + } + + if (!getDataSourceEnabled().enabled) { + throw new Error('To query from multiple data sources, first enable the data source feature'); + } + + const dataSources = await client.find({ + type: 'data-source', + perPage: 100, + search: `"${config.data_source_name}"`, + searchFields: ['title'], + fields: ['id', 'title'], + }); + + const possibleDataSourceIds = dataSources.saved_objects.filter( + (obj) => obj.attributes.title === config.data_source_name + ); + + if (possibleDataSourceIds.length !== 1) { + throw new Error( + `Expected exactly 1 result for data_source_name "${config.data_source_name}" but got ${possibleDataSourceIds.length} results` + ); + } + + return possibleDataSourceIds.pop()?.id; +}; diff --git a/src/plugins/vis_type_timeline/server/lib/services.ts b/src/plugins/vis_type_timeline/server/lib/services.ts new file mode 100644 index 000000000000..13b257622abd --- /dev/null +++ b/src/plugins/vis_type_timeline/server/lib/services.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createGetterSetter } from '../../../opensearch_dashboards_utils/common'; + +export const [getDataSourceEnabled, setDataSourceEnabled] = createGetterSetter<{ + enabled: boolean; +}>('DataSource'); diff --git a/src/plugins/vis_type_timeline/server/plugin.ts b/src/plugins/vis_type_timeline/server/plugin.ts index d2c7097ac419..f768e51e93d0 100644 --- a/src/plugins/vis_type_timeline/server/plugin.ts +++ b/src/plugins/vis_type_timeline/server/plugin.ts @@ -34,6 +34,7 @@ import { TypeOf, schema } from '@osd/config-schema'; import { RecursiveReadonly } from '@osd/utility-types'; import { deepFreeze } from '@osd/std'; +import { DataSourcePluginSetup } from 'src/plugins/data_source/server'; import { PluginStart } from '../../data/server'; import { CoreSetup, PluginInitializerContext } from '../../../core/server'; import { configSchema } from '../config'; @@ -42,11 +43,16 @@ import { functionsRoute } from './routes/functions'; import { validateOpenSearchRoute } from './routes/validate_es'; import { runRoute } from './routes/run'; import { ConfigManager } from './lib/config_manager'; +import { setDataSourceEnabled } from './lib/services'; const experimentalLabel = i18n.translate('timeline.uiSettings.experimentalLabel', { defaultMessage: 'experimental', }); +export interface TimelinePluginSetupDeps { + dataSource?: DataSourcePluginSetup; +} + export interface TimelinePluginStartDeps { data: PluginStart; } @@ -57,7 +63,7 @@ export interface TimelinePluginStartDeps { export class Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup(core: CoreSetup): void { + public async setup(core: CoreSetup, { dataSource }: TimelinePluginSetupDeps): void { const config = await this.initializerContext.config .create>() .pipe(first()) @@ -80,6 +86,8 @@ export class Plugin { ); }; + setDataSourceEnabled({ enabled: !!dataSource }); + const logger = this.initializerContext.logger.get('timeline'); const router = core.http.createRouter(); diff --git a/src/plugins/vis_type_timeline/server/routes/run.ts b/src/plugins/vis_type_timeline/server/routes/run.ts index ab6a993b4bb5..af1005ebcb8f 100644 --- a/src/plugins/vis_type_timeline/server/routes/run.ts +++ b/src/plugins/vis_type_timeline/server/routes/run.ts @@ -122,7 +122,10 @@ export function runRoute( } else { return response.internalError({ body: { - message: err.toString(), + attributes: { + title: err.name, + }, + message: err.message, }, }); } diff --git a/src/plugins/vis_type_timeline/server/series_functions/opensearch/index.js b/src/plugins/vis_type_timeline/server/series_functions/opensearch/index.js index 8837116bfc02..5192059f3e6d 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/opensearch/index.js +++ b/src/plugins/vis_type_timeline/server/series_functions/opensearch/index.js @@ -34,6 +34,7 @@ import { OPENSEARCH_SEARCH_STRATEGY } from '../../../../data/server'; import Datasource from '../../lib/classes/datasource'; import buildRequest from './lib/build_request'; import toSeriesList from './lib/agg_response_to_series_list'; +import { fetchDataSourceIdByName } from '../../lib/fetch_data_source_id'; export default new Datasource('es', { args: [ @@ -112,6 +113,14 @@ export default new Datasource('es', { defaultMessage: `**DO NOT USE THIS**. It's fun for debugging fit functions, but you really should use the interval picker`, }), }, + { + name: 'data_source_name', + types: ['string', 'null'], // If null, the query will proceed with local cluster + help: i18n.translate('timeline.help.functions.opensearch.args.dataSourceNameHelpText', { + defaultMessage: + 'Specify a data source to query from. This will only work if multiple data sources is enabled', + }), + }, ], help: i18n.translate('timeline.help.functions.opensearchHelpText', { defaultMessage: 'Pull data from an opensearch instance', @@ -148,7 +157,15 @@ export default new Datasource('es', { const opensearchShardTimeout = tlConfig.opensearchShardTimeout; - const body = buildRequest(config, tlConfig, scriptedFields, opensearchShardTimeout); + const dataSourceId = await fetchDataSourceIdByName(config, tlConfig.savedObjectsClient); + + const body = buildRequest( + config, + tlConfig, + scriptedFields, + opensearchShardTimeout, + dataSourceId + ); const deps = (await tlConfig.getStartServices())[1]; diff --git a/src/plugins/vis_type_timeline/server/series_functions/opensearch/lib/build_request.js b/src/plugins/vis_type_timeline/server/series_functions/opensearch/lib/build_request.js index 8436b4dbb04a..90fb7b819a08 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/opensearch/lib/build_request.js +++ b/src/plugins/vis_type_timeline/server/series_functions/opensearch/lib/build_request.js @@ -34,7 +34,7 @@ import { buildAggBody } from './agg_body'; import createDateAgg from './create_date_agg'; import { UI_SETTINGS } from '../../../../../data/server'; -export default function buildRequest(config, tlConfig, scriptedFields, timeout) { +export default function buildRequest(config, tlConfig, scriptedFields, timeout, dataSourceId) { const bool = { must: [] }; const timeFilter = { @@ -105,6 +105,7 @@ export default function buildRequest(config, tlConfig, scriptedFields, timeout) } return { + ...(!!dataSourceId && { dataSourceId }), params: request, }; } diff --git a/src/plugins/vis_type_timeline/server/types.ts b/src/plugins/vis_type_timeline/server/types.ts index f021ffeae00f..2fa3a25c5813 100644 --- a/src/plugins/vis_type_timeline/server/types.ts +++ b/src/plugins/vis_type_timeline/server/types.ts @@ -29,3 +29,18 @@ */ export { TimelineFunctionInterface, TimelineFunctionConfig } from './lib/classes/timeline_function'; + +export interface OpenSearchFunctionConfig { + q: string | null; + metric: string | null; + split: string | null; + index: string | null; + timefield: string | null; + kibana: boolean | null; + opensearchDashboards: boolean | null; + /** + * @deprecated This property should not be set in the Timeline expression. Users should use the interval picker React component instead + */ + interval: string | null; + data_source_name?: string | null; +} From b619ccb5fea8b23a748d74ba3dec21aabb1a7765 Mon Sep 17 00:00:00 2001 From: "Yuanqi(Ella) Zhu" <53279298+zhyuanqi@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:38:25 -0700 Subject: [PATCH 6/7] Modify the button of selectable component to fix the title overflow issue (#6465) * Modify the button of selectable component to fix the title overflow issue Signed-off-by: Yuanqi(Ella) Zhu * update snapshot Signed-off-by: Yuanqi(Ella) Zhu --------- Signed-off-by: Yuanqi(Ella) Zhu --- CHANGELOG.md | 1 + .../public/components/button_title.scss | 7 +++++++ .../create_data_source_menu.test.tsx.snap | 6 +++--- .../__snapshots__/data_source_selectable.test.tsx.snap | 10 +++++----- .../data_source_selectable/data_source_selectable.tsx | 5 +++-- 5 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/button_title.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf4d4b0570d..f49b43d1e946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [BUG][Multiple Datasource] Refactor read-only component to cover more edge cases ([#6416](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6416)) - [BUG] Fix for checkForFunctionProperty so that order does not matter ([#6248](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6248)) - [Dynamic Configurations] Fix dynamic config API calls to pass correct input ([#6474](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6474)) +- [BUG][Multiple Datasource] Modify the button of selectable component to fix the title overflow issue ([#6465](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6465)) - [BUG][Multiple Datasource] Validation succeed as long as status code in response is 200 ([#6399](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6399)) - [BUG][Multiple Datasource] Add validation for title length to be no longer than 32 characters [#6452](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6452)) diff --git a/src/plugins/data_source_management/public/components/button_title.scss b/src/plugins/data_source_management/public/components/button_title.scss new file mode 100644 index 000000000000..66b32d4ee8b7 --- /dev/null +++ b/src/plugins/data_source_management/public/components/button_title.scss @@ -0,0 +1,7 @@ +.dataSourceComponentButtonTitle { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: auto; + max-width: 16ch; +} diff --git a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap index c520768a6890..c705db9194b0 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap @@ -104,7 +104,7 @@ Object { >