From e3a8a215686f8be69ff65f2f4c5bd9c6043800e3 Mon Sep 17 00:00:00 2001 From: yujin-emma Date: Thu, 25 Apr 2024 22:02:39 +0000 Subject: [PATCH 01/27] adjust the aggregated view padding Signed-off-by: yujin-emma --- .../data_source_aggregated_view.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss index 62f6239424aa..5a33d167ac9a 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss @@ -11,10 +11,11 @@ .euiSelectableListItem__content { cursor: default; + padding-left: 0%; .euiSelectableListItem__text { - max-height: 100%; + max-height: 100%; } } From b51db9eada3899647563380810daedb50049016c Mon Sep 17 00:00:00 2001 From: yujin-emma Date: Fri, 26 Apr 2024 00:32:35 +0000 Subject: [PATCH 02/27] WIP Signed-off-by: yujin-emma --- CHANGELOG.md | 1 + .../data_source_aggregated_view.scss | 9 ++++++++- .../data_source_aggregated_view.tsx | 6 +++++- .../data_source_item/data_source_item.scss | 17 +++++++++++++++++ .../data_source_item/data_source_item.tsx | 1 + 5 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b63850e98e..4514f596e7cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) +- [Multiple Datasource] Adjust the padding size for aggregated view ([#6715](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6715)) - [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179)) - [Workspace] Add update workspace page ([#6270](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6270)) - [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss index 5a33d167ac9a..15aa1a8c9fcc 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss @@ -11,12 +11,18 @@ .euiSelectableListItem__content { cursor: default; - padding-left: 0%; + padding-left: 0; + margin-right: 0; .euiSelectableListItem__text { max-height: 100%; } + .euiSelectableListItem__icon, .euiSelectableListItem__prepend { + margin-right: 0; + } + + } .dataSourceAggregatedViewOuiFlexGroup { @@ -32,6 +38,7 @@ overflow: hidden; white-space: nowrap; display: inline-block; + padding-left: 0%; } } 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 00eacb2ea844..7357364c4b54 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 @@ -225,7 +225,11 @@ export class DataSourceAggregatedView extends React.Component< options={items} renderOption={(option) => ( diff --git a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss new file mode 100644 index 000000000000..66149e36bbc0 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss @@ -0,0 +1,17 @@ +.dataSourceAggregatedViewOuiFlexItem { + color: grey; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + padding-left: 0%; +} + +.dataSourceListAllActiveOuiFlexItem { + color: grey; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + padding-left: 1em; +} diff --git a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx index 06064ccd1836..47af64c99988 100644 --- a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx +++ b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { EuiBadge, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { DataSourceOption } from '../data_source_menu/types'; +import './data_source_item.scss'; interface DataSourceItemProps { className: string; From f701566051567624cbc094ae75d474d564df1239 Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Fri, 3 May 2024 15:52:10 -0700 Subject: [PATCH 03/27] Update src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx Co-authored-by: Xinrui Bai-amazon <139305463+xinruiba@users.noreply.github.com> Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Signed-off-by: yujin-emma --- .../data_source_aggregated_view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 7357364c4b54..caaaac665a8d 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 @@ -226,9 +226,9 @@ export class DataSourceAggregatedView extends React.Component< renderOption={(option) => ( Date: Tue, 7 May 2024 20:04:44 +0000 Subject: [PATCH 04/27] Changeset file for PR #6715 created/updated Signed-off-by: yujin-emma --- changelogs/fragments/6715.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/6715.yml diff --git a/changelogs/fragments/6715.yml b/changelogs/fragments/6715.yml new file mode 100644 index 000000000000..4d5c07f57bd5 --- /dev/null +++ b/changelogs/fragments/6715.yml @@ -0,0 +1,2 @@ +fix: +- Adjust the padding size for aggregated view ([#6715](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6715)) \ No newline at end of file From 1b09d37efc813fee8ec8028d17f4a98f91db70ad Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Tue, 7 May 2024 13:04:56 -0700 Subject: [PATCH 05/27] Update CHANGELOG.md Signed-off-by: yujin-emma --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4514f596e7cc..08b63850e98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,6 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) -- [Multiple Datasource] Adjust the padding size for aggregated view ([#6715](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6715)) - [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179)) - [Workspace] Add update workspace page ([#6270](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6270)) - [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) From e8776fe46b438bd5a504dc1358dce704dcd0971d Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Sat, 4 May 2024 17:48:58 -0700 Subject: [PATCH 06/27] [saved objects] enable deletion of saved objects by type if configured (#6443) * [saved objects] enable deletion of saved objects by type if configured Adds the following settings: ``` migrations.delete.enabled migrations.delete.types ``` `unknown` types already exist but the purpose of this type is for plugins that are disabled. OpenSearch Dashboards gets confused when a plugin is not defining a saved object type but the saved object exists. This can occur when migrating from a non-OSD version and there exists non-compatiable saved objects. If OSD is failing to migrate an index because of a document, I can now configure OSD to delete types of saved objects that I specified because I know that these types should not be carried over. resolves: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1040 Signed-off-by: Kawika Avilla * address comments Signed-off-by: Kawika Avilla * Changeset file for PR #6443 created/updated --------- Signed-off-by: Kawika Avilla Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6443.yml | 2 + config/opensearch_dashboards.yml | 8 +- .../migrations/core/index_migrator.test.ts | 222 + .../migrations/core/index_migrator.ts | 29 + .../migrations/core/migration_context.test.ts | 87 +- .../migrations/core/migration_context.ts | 57 +- .../core/migration_opensearch_client.test.ts | 4 + .../core/migration_opensearch_client.ts | 2 + .../opensearch_dashboards_migrator.test.ts | 42 +- .../opensearch_dashboards_migrator.ts | 3 + .../saved_objects/saved_objects_config.ts | 13 + .../bin/opensearch-dashboards-docker | 2 + .../dashboard_listing.test.tsx.snap | 8203 ++++++++++++++++- 13 files changed, 8660 insertions(+), 14 deletions(-) create mode 100644 changelogs/fragments/6443.yml diff --git a/changelogs/fragments/6443.yml b/changelogs/fragments/6443.yml new file mode 100644 index 000000000000..2de7191e0d09 --- /dev/null +++ b/changelogs/fragments/6443.yml @@ -0,0 +1,2 @@ +feat: +- Adds `migrations.delete` to delete saved objects by type during a migration ([#6443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6443)) \ No newline at end of file diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index a8ef9638ce76..3066cff7ed0a 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -314,6 +314,12 @@ # Set the value to true to enable workspace feature # workspace.enabled: false +# Optional settings to specify saved object types to be deleted during migration. +# This feature can help address compatibility issues that may arise during the migration of saved objects, such as types defined by legacy applications. +# Please note, using this feature carries a risk. Deleting saved objects during migration could potentially lead to unintended data loss. Use with caution. +# migrations.delete.enabled: false +# migrations.delete.types: [] + # Set the value to true to enable Ui Metric Collectors in Usage Collector # This publishes the Application Usage and UI Metrics into the saved object, which can be accessed by /api/stats?extended=true&legacy=true&exclude_usage=false -# usageCollection.uiMetric.enabled: false \ No newline at end of file +# usageCollection.uiMetric.enabled: false diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 8b1f5df9640a..d55959ef769c 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -435,6 +435,228 @@ describe('IndexMigrator', () => { }); }); + test('deletes saved objects by type if configured', async () => { + const { client } = testOpts; + + const deleteType = 'delete_type'; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return [deleteType]; + } + }); + testOpts.opensearchDashboardsRawConfig = rawConfig; + + testOpts.mappingProperties = { foo: { type: 'text' } as any }; + + withIndex(client, { + index: { + '.kibana_1': { + aliases: {}, + mappings: { + properties: { + delete_type: { properties: { type: deleteType } }, + }, + }, + }, + }, + }); + + await new IndexMigrator(testOpts).migrate(); + + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + _meta: { + migrationMappingPropertyHashes: { + foo: '625b32086eb1d1203564cf85062dd22e', + migrationVersion: '4a1746014a75ade3a714e1db5763276f', + namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', + references: '7997cf5a56cc02bdc9c93361bde732b0', + type: '2f4316de49999235636386fe51dc06c1', + updated_at: '00da57df13e94e9d98437d13ace4bfe0', + }, + }, + properties: { + foo: { type: 'text' }, + migrationVersion: { dynamic: 'true', type: 'object' }, + namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, + type: { type: 'keyword' }, + updated_at: { type: 'date' }, + references: { + type: 'nested', + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, + }, + }, + settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, + }, + index: '.kibana_2', + }); + }); + + test('retains saved objects by type if delete is not enabled', async () => { + const { client } = testOpts; + + const deleteType = 'delete_type'; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return false; + } + if (path === 'migrations.delete.types') { + return [deleteType]; + } + }); + testOpts.opensearchDashboardsRawConfig = rawConfig; + + testOpts.mappingProperties = { foo: { type: 'text' } as any }; + + withIndex(client, { + index: { + '.kibana_1': { + aliases: {}, + mappings: { + properties: { + delete_type: { properties: { type: deleteType } }, + }, + }, + }, + }, + }); + + await new IndexMigrator(testOpts).migrate(); + + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + _meta: { + migrationMappingPropertyHashes: { + foo: '625b32086eb1d1203564cf85062dd22e', + migrationVersion: '4a1746014a75ade3a714e1db5763276f', + namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', + references: '7997cf5a56cc02bdc9c93361bde732b0', + type: '2f4316de49999235636386fe51dc06c1', + updated_at: '00da57df13e94e9d98437d13ace4bfe0', + }, + }, + properties: { + delete_type: { dynamic: false, properties: {} }, + foo: { type: 'text' }, + migrationVersion: { dynamic: 'true', type: 'object' }, + namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, + type: { type: 'keyword' }, + updated_at: { type: 'date' }, + references: { + type: 'nested', + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, + }, + }, + settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, + }, + index: '.kibana_2', + }); + }); + + test('retains saved objects by type if delete types does not exist', async () => { + const { client } = testOpts; + + const deleteType = 'delete_type'; + const retainType = 'retain_type'; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return [deleteType]; + } + }); + testOpts.opensearchDashboardsRawConfig = rawConfig; + + testOpts.mappingProperties = { foo: { type: 'text' } as any }; + + withIndex(client, { + index: { + '.kibana_1': { + aliases: {}, + mappings: { + properties: { + retain_type: { properties: { type: retainType } }, + }, + }, + }, + }, + }); + + await new IndexMigrator(testOpts).migrate(); + + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + _meta: { + migrationMappingPropertyHashes: { + foo: '625b32086eb1d1203564cf85062dd22e', + migrationVersion: '4a1746014a75ade3a714e1db5763276f', + namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', + references: '7997cf5a56cc02bdc9c93361bde732b0', + type: '2f4316de49999235636386fe51dc06c1', + updated_at: '00da57df13e94e9d98437d13ace4bfe0', + }, + }, + properties: { + retain_type: { dynamic: false, properties: {} }, + foo: { type: 'text' }, + migrationVersion: { dynamic: 'true', type: 'object' }, + namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, + type: { type: 'keyword' }, + updated_at: { type: 'date' }, + references: { + type: 'nested', + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, + }, + }, + settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, + }, + index: '.kibana_2', + }); + }); + test('points the alias at the dest index', async () => { const { client } = testOpts; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index 1a616f8a2c7d..20784d6db8f6 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -28,6 +28,7 @@ * under the License. */ +import { DeleteByQueryRequest } from '@opensearch-project/opensearch/api/types'; import { diffMappings } from './build_active_mappings'; import * as Index from './opensearch_index'; import { migrateRawDocs } from './migrate_raw_docs'; @@ -123,6 +124,7 @@ async function migrateIndex(context: Context): Promise { const { client, alias, source, dest, log } = context; await deleteIndexTemplates(context); + await deleteSavedObjectsByType(context); log.info(`Creating index ${dest.indexName}.`); @@ -171,6 +173,33 @@ async function deleteIndexTemplates({ client, log, obsoleteIndexTemplatePattern return Promise.all(templateNames.map((name) => client.indices.deleteTemplate({ name: name! }))); } +/** + * Delete saved objects by type. If migrations.delete.types is specified, + * any saved objects that matches that type will be deleted. + */ +async function deleteSavedObjectsByType(context: Context) { + const { client, source, log, typesToDelete } = context; + if (!source.exists || !typesToDelete || typesToDelete.length === 0) { + return; + } + + log.info(`Removing saved objects of types: ${typesToDelete.join(', ')}`); + const params = { + index: source.indexName, + body: { + query: { + bool: { + should: [...typesToDelete.map((type) => ({ term: { type } }))], + }, + }, + }, + conflicts: 'proceed', + refresh: true, + } as DeleteByQueryRequest; + log.debug(`Delete by query params: ${JSON.stringify(params)}`); + return client.deleteByQuery(params); +} + /** * Moves all docs from sourceIndex to destIndex, migrating each as necessary. * This moves documents from the concrete index, rather than the alias, to prevent diff --git a/src/core/server/saved_objects/migrations/core/migration_context.test.ts b/src/core/server/saved_objects/migrations/core/migration_context.test.ts index 71db15842cd3..c30e6910cbf4 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.test.ts @@ -28,7 +28,8 @@ * under the License. */ -import { disableUnknownTypeMappingFields } from './migration_context'; +import { disableUnknownTypeMappingFields, deleteTypeMappingsFields } from './migration_context'; +import { configMock } from '../../../config/mocks'; describe('disableUnknownTypeMappingFields', () => { const sourceMappings = { @@ -97,3 +98,87 @@ describe('disableUnknownTypeMappingFields', () => { }); }); }); + +describe('deleteTypeMappingsFields', () => { + it('should delete specified type mappings fields', () => { + const targetMappings = { + properties: { + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }, + } as const; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return ['type1', 'type3']; + } + }); + + const updatedMappings = deleteTypeMappingsFields(targetMappings, rawConfig); + + expect(updatedMappings.properties).toEqual({ + type2: { type: 'keyword' }, + }); + }); + + it('should not delete any type mappings fields if delete is not enabled', () => { + const targetMappings = { + properties: { + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }, + } as const; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return false; + } + if (path === 'migrations.delete.types') { + return ['type1', 'type3']; + } + }); + + const updatedMappings = deleteTypeMappingsFields(targetMappings, rawConfig); + + expect(updatedMappings.properties).toEqual({ + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }); + }); + + it('should not delete any type mappings fields if delete types are not specified', () => { + const targetMappings = { + properties: { + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }, + } as const; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return []; + } + }); + + const updatedMappings = deleteTypeMappingsFields(targetMappings, rawConfig); + + expect(updatedMappings.properties).toEqual({ + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index 91114701d95f..987115c2ce08 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -66,6 +66,11 @@ export interface MigrationOpts { * prior to running migrations. For example: 'opensearch_dashboards_index_template*' */ obsoleteIndexTemplatePattern?: string; + /** + * If specified, types matching the specified list will be removed prior to + * running migrations. Useful for removing types that are not supported. + */ + typesToDelete?: string[]; opensearchDashboardsRawConfig?: Config; } @@ -84,6 +89,7 @@ export interface Context { scrollDuration: string; serializer: SavedObjectsSerializer; obsoleteIndexTemplatePattern?: string; + typesToDelete?: string[]; convertToAliasScript?: string; } @@ -114,6 +120,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { scrollDuration: opts.scrollDuration, serializer: opts.serializer, obsoleteIndexTemplatePattern: opts.obsoleteIndexTemplatePattern, + typesToDelete: opts.typesToDelete, convertToAliasScript: opts.convertToAliasScript, }; } @@ -135,9 +142,12 @@ function createDestContext( typeMappingDefinitions: SavedObjectsTypeMappingDefinitions, opensearchDashboardsRawConfig?: Config ): Index.FullIndexInfo { - const targetMappings = disableUnknownTypeMappingFields( - buildActiveMappings(typeMappingDefinitions, opensearchDashboardsRawConfig), - source.mappings + const targetMappings = deleteTypeMappingsFields( + disableUnknownTypeMappingFields( + buildActiveMappings(typeMappingDefinitions, opensearchDashboardsRawConfig), + source.mappings + ), + opensearchDashboardsRawConfig ); return { @@ -162,7 +172,7 @@ function createDestContext( * type's mappings are set to `dynamic: false`. * * (Since we're using the source index mappings instead of looking at actual - * document types in the inedx, we potentially add more "unknown types" than + * document types in the index, we potentially add more "unknown types" than * what would be necessary to support migrating all the data over to the * target index.) * @@ -199,6 +209,43 @@ export function disableUnknownTypeMappingFields( }; } +/** + * This function is used to modify the target mappings object by deleting specified type mappings fields. + * + * The function operates under the following conditions: + * - It checks if the 'migrations.delete.enabled' configuration is set to true. + * - If true, it retrieves the 'migrations.delete.types' configuration + * - For each type, it deletes the corresponding property from the target mappings object. + * + * The purpose of this function is to allow for dynamic modification of the target mappings object + * based on the application's configuration. This can be useful in scenarios where certain type + * mappings are no longer needed and should be removed from the target mappings. + * + * @param {Object} targetMappings - The target mappings object to be modified. + * @param {Object} opensearchDashboardsRawConfig - The application's configuration object. + * @returns The mappings that should be applied to the target index. + */ +export function deleteTypeMappingsFields( + targetMappings: IndexMapping, + opensearchDashboardsRawConfig?: Config +) { + if (opensearchDashboardsRawConfig?.get('migrations.delete.enabled')) { + const deleteTypes = new Set(opensearchDashboardsRawConfig.get('migrations.delete.types')); + const newProperties = Object.keys(targetMappings.properties) + .filter((key) => !deleteTypes.has(key)) + .reduce((obj, key) => { + return { ...obj, [key]: targetMappings.properties[key] }; + }, {}); + + return { + ...targetMappings, + properties: newProperties, + }; + } + + return targetMappings; +} + /** * Gets the next index name in a sequence, based on specified current index's info. * We're using a numeric counter to create new indices. So, `.opensearch_dashboards_1`, `.opensearch_dashboards_2`, etc @@ -206,6 +253,6 @@ export function disableUnknownTypeMappingFields( */ function nextIndexName(indexName: string, alias: string) { const indexSuffix = (indexName.match(/[\d]+$/) || [])[0]; - const indexNum = parseInt(indexSuffix, 10) || 0; + const indexNum = parseInt(indexSuffix!, 10) || 0; return `${alias}_${indexNum + 1}`; } diff --git a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts index 91f11cbd4878..8675f86c10ea 100644 --- a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts @@ -76,4 +76,8 @@ describe('MigrationOpenSearchClient', () => { expect(SavedObjectsErrorHelpers.isSavedObjectsClientError(e)).toBe(false); } }); + + it('should have the deleteByQuery method', () => { + expect(client.deleteByQuery).toBeDefined(); + }); }); diff --git a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts index 7ab77d5a62dd..4cb4fef39de3 100644 --- a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts +++ b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts @@ -51,6 +51,7 @@ const methods = [ 'search', 'scroll', 'tasks.get', + 'deleteByQuery', ] as const; type MethodName = typeof methods[number]; @@ -77,6 +78,7 @@ export interface MigrationOpenSearchClient { tasks: { get: OpenSearchClient['tasks']['get']; }; + deleteByQuery: OpenSearchClient['deleteByQuery']; } export function createMigrationOpenSearchClient( diff --git a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts index e65effdd8eaa..0a52b1947f2f 100644 --- a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts @@ -89,6 +89,12 @@ describe('OpenSearchDashboardsMigrator', () => { const mappings = new OpenSearchDashboardsMigrator(options).getActiveMappings(); expect(mappings).toHaveProperty('properties.workspaces'); }); + + it('text field does not exist in the mappings when the feature is enabled', () => { + const options = mockOptions(false, false, { enabled: true, types: ['text'] }); + const mappings = new OpenSearchDashboardsMigrator(options).getActiveMappings(); + expect(mappings).not.toHaveProperty('properties.text'); + }); }); describe('runMigrations', () => { @@ -159,10 +165,14 @@ type MockedOptions = OpenSearchDashboardsMigratorOptions & { client: ReturnType; }; -const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: boolean) => { +const mockOptions = ( + isWorkspaceEnabled?: boolean, + isPermissionControlEnabled?: boolean, + deleteConfig?: { enabled: boolean; types: string[] } +) => { const rawConfig = configMock.create(); rawConfig.get.mockReturnValue(false); - if (isWorkspaceEnabled || isPermissionControlEnabled) { + if (isWorkspaceEnabled || isPermissionControlEnabled || deleteConfig?.enabled) { rawConfig.get.mockReturnValue(true); } rawConfig.get.mockImplementation((path) => { @@ -178,6 +188,18 @@ const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: } else { return false; } + } else if (path === 'migrations.delete.enabled') { + if (deleteConfig?.enabled) { + return true; + } else { + return false; + } + } else if (path === 'migrations.delete.types') { + if (deleteConfig?.enabled) { + return deleteConfig?.types; + } else { + return []; + } } else { return false; } @@ -209,6 +231,18 @@ const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: }, migrations: {}, }, + { + name: 'testtype3', + hidden: false, + namespaceType: 'single', + indexPattern: 'other-index', + mappings: { + properties: { + name: { type: 'text' }, + }, + }, + migrations: {}, + }, ]), opensearchDashboardsConfig: { enabled: true, @@ -219,6 +253,10 @@ const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: pollInterval: 20000, scrollDuration: '10m', skip: false, + delete: { + enabled: rawConfig.get('migrations.delete.enabled'), + types: rawConfig.get('migrations.delete.types'), + }, }, client: opensearchClientMock.createOpenSearchClient(), opensearchDashboardsRawConfig: rawConfig, diff --git a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts index d6c119569a2e..e0e623f20f94 100644 --- a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts +++ b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts @@ -187,6 +187,9 @@ export class OpenSearchDashboardsMigrator { index === opensearchDashboardsIndexName ? 'opensearch_dashboards_index_template*' : undefined, + typesToDelete: this.savedObjectsConfig.delete.enabled + ? this.savedObjectsConfig.delete.types + : undefined, convertToAliasScript: indexMap[index].script, opensearchDashboardsRawConfig: this.opensearchDashboardsRawConfig, }); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index e6ffaefb8a59..ccf95b21cd45 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -39,6 +39,19 @@ export const savedObjectsMigrationConfig = { scrollDuration: schema.string({ defaultValue: '15m' }), pollInterval: schema.number({ defaultValue: 1500 }), skip: schema.boolean({ defaultValue: false }), + delete: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + types: schema.arrayOf(schema.string(), { defaultValue: [] }), + }, + { + validate(value) { + if (value.enabled === true && value.types.length === 0) { + return 'delete types cannot be empty when delete is enabled'; + } + }, + } + ), }), }; diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker index 124a5e074842..c9cf5d1213c0 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker @@ -76,6 +76,8 @@ opensearch_dashboards_vars=( map.tilemap.options.minZoom map.tilemap.options.subdomains map.tilemap.url + migrations.delete.enabled + migrations.delete.types monitoring.cluster_alerts.email_notifications.email_address monitoring.enabled monitoring.opensearchDashboards.collection.enabled diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index b0ee4d34705f..c853b9fc7941 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -1129,11 +1129,1481 @@ exports[`dashboard listing hideWriteControls 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -2335,11 +3805,2151 @@ exports[`dashboard listing render table listing with initial filters from URL 1` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "dashboard", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -3541,11 +7151,274 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = data-test-subj="dashboardLandingPage" >
+ > + + +
+ + + + } + body={ + +

+ +

+

+ + + , + } + } + /> +

+
+ } + iconType="dashboardApp" + title={ +

+ +

+ } + > +
+ + + + +
+ + +

+ + Create your first dashboard + +

+
+ + + +
+ + +
+

+ + You can combine data views from any OpenSearch Dashboards app into one dashboard and see everything in one place. + +

+

+ + + , + } + } + > + New to OpenSearch Dashboards? + + + + to take a test drive. + +

+
+
+ + + +
+ + + + + + +
+ +
+ + +
@@ -4747,11 +8620,2111 @@ exports[`dashboard listing renders table rows 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -5953,11 +11926,2231 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + > +
+
+ + + + Listing limit exceeded + + +
+ +
+ +
+

+ + + , + "entityNamePlural": "dashboards", + "listingLimitText": + listingLimit + , + "listingLimitValue": 1, + "totalItems": 2, + } + } + > + You have 2 dashboards, but your + + listingLimit + + setting prevents the table below from displaying more than 1. You can change this setting under + + + + Advanced Settings + + + + . + +

+
+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
From ab37eb2dfe6edad7d1fd7b9985cfd37f956fc2f9 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Mon, 6 May 2024 13:58:13 -0700 Subject: [PATCH 07/27] [tests] fix tests related to #6443 (#6722) * [tests] fix tests related to #6443 PR: 6443 merged in previous PR that had an immediate follow up PR to that PR. But the followup pr was not merged into 6443. The snapshots got updated to ensure the CI passed for 6443. But once merged into main, failures were observed. Signed-off-by: Kawika Avilla * Changeset file for PR #6722 created/updated --------- Signed-off-by: Kawika Avilla Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6722.yml | 2 + .../dashboard_listing.test.tsx.snap | 8205 +---------------- 2 files changed, 8 insertions(+), 8199 deletions(-) create mode 100644 changelogs/fragments/6722.yml diff --git a/changelogs/fragments/6722.yml b/changelogs/fragments/6722.yml new file mode 100644 index 000000000000..6e4c4511ac0a --- /dev/null +++ b/changelogs/fragments/6722.yml @@ -0,0 +1,2 @@ +fix: +- Test failures related to #6443 ([#6722](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6722)) \ No newline at end of file diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index c853b9fc7941..db34c4f229bb 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -1129,1481 +1129,11 @@ exports[`dashboard listing hideWriteControls 1`] = ` data-test-subj="dashboardLandingPage" >
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
@@ -3805,2151 +2335,11 @@ exports[`dashboard listing render table listing with initial filters from URL 1` data-test-subj="dashboardLandingPage" >
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
- - -
- - - - - -
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "dashboard", - "onChange": [Function], - "toolsLeft": undefined, - } - } - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
- - - - - - - - - - - - - - Actions - - - - - -
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
@@ -7151,274 +3541,11 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = data-test-subj="dashboardLandingPage" >
- - -
- - - - } - body={ - -

- -

-

- - - , - } - } - /> -

-
- } - iconType="dashboardApp" - title={ -

- -

- } - > -
- - - - -
- - -

- - Create your first dashboard - -

-
- - - -
- - -
-

- - You can combine data views from any OpenSearch Dashboards app into one dashboard and see everything in one place. - -

-

- - - , - } - } - > - New to OpenSearch Dashboards? - - - - to take a test drive. - -

-
-
- - - -
- - - - - - -
- -
- - -
+ />
@@ -8620,2111 +4747,11 @@ exports[`dashboard listing renders table rows 1`] = ` data-test-subj="dashboardLandingPage" >
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
- - -
- - - - - -
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, - } - } - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
- - - - - - - - - - - - - - Actions - - - - - -
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
@@ -11926,2231 +5953,11 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` data-test-subj="dashboardLandingPage" >
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
- - -
- - - - - -
-
-
-
-
- -
- - - } - > -
-
- - - - Listing limit exceeded - - -
- -
- -
-

- - - , - "entityNamePlural": "dashboards", - "listingLimitText": - listingLimit - , - "listingLimitValue": 1, - "totalItems": 2, - } - } - > - You have 2 dashboards, but your - - listingLimit - - setting prevents the table below from displaying more than 1. You can change this setting under - - - - Advanced Settings - - - - . - -

-
-
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, - } - } - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
- - - - - - - - - - - - - - Actions - - - - - -
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
@@ -14160,4 +5967,4 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` -`; \ No newline at end of file +`; From 1bbabe2f7987de15cb6d9249e3cb88f697c00aa0 Mon Sep 17 00:00:00 2001 From: tygao Date: Tue, 7 May 2024 10:33:01 +0800 Subject: [PATCH 08/27] feat: add config for topRightNavigation (#6712) * feat: add config for topRightNavigation Signed-off-by: tygao * doc: update changelog Signed-off-by: tygao * Changeset file for PR #6712 created/updated * test: update context test Signed-off-by: tygao * doc: update changelog Signed-off-by: tygao * update changeset Signed-off-by: tygao * update joi default value Signed-off-by: tygao * add experimental annotation and add futureNavigation configuration Signed-off-by: tygao * add experimental annotation and add futureNavigation configuration Signed-off-by: tygao --------- Signed-off-by: tygao Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Ashwin P Chandran Signed-off-by: yujin-emma --- CHANGELOG.md | 1 + changelogs/fragments/6712.yml | 2 ++ config/opensearch_dashboards.yml | 3 ++ .../ui/header/right_navigation_button.tsx | 9 +++++- src/core/server/mocks.ts | 1 + .../server/opensearch_dashboards_config.ts | 1 + .../server/plugins/plugin_context.test.ts | 1 + src/core/server/plugins/types.ts | 1 + src/legacy/server/config/schema.js | 1 + src/plugins/console/server/plugin.ts | 9 +++--- src/plugins/dev_tools/public/plugin.ts | 29 ++++++++++--------- 11 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 changelogs/fragments/6712.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b63850e98e..aa524b4ec4dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Support multi data source in Region map ([#6654](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6654)) - Add `rightNavigationButton` component in chrome service for applications to register and add dev tool to top right navigation. ([#6553](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6553)) - Enable UI Metric Collector to collect UI Metrics and Application Usage ([#6203](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6203)) +- Add `opensearchDashboards.futureNavigation` config to control dev tool top right nav button. ([#6712](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6712)) ### 🐛 Bug Fixes diff --git a/changelogs/fragments/6712.yml b/changelogs/fragments/6712.yml new file mode 100644 index 000000000000..36e5ebc65001 --- /dev/null +++ b/changelogs/fragments/6712.yml @@ -0,0 +1,2 @@ +feat: +- Add `opensearchDashboards.futureNavigation` config to control dev tool top right nav button. ([#6712](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6712)) \ No newline at end of file diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 3066cff7ed0a..f94234cffe1b 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -303,6 +303,9 @@ # Set the value of this setting to false to hide the help menu link to the OpenSearch Dashboards user survey # opensearchDashboards.survey.url: "https://survey.opensearch.org" +# @experimental Set the value of this setting to display navigation updates, including dev tool top right navigation +# opensearchDashboards.futureNavigation: false + # Set the value of this setting to true to enable plugin augmentation on Dashboard # vis_augmenter.pluginAugmentationEnabled: true diff --git a/src/core/public/chrome/ui/header/right_navigation_button.tsx b/src/core/public/chrome/ui/header/right_navigation_button.tsx index 31464759aea5..9fa98109e313 100644 --- a/src/core/public/chrome/ui/header/right_navigation_button.tsx +++ b/src/core/public/chrome/ui/header/right_navigation_button.tsx @@ -6,8 +6,12 @@ import { EuiHeaderSectionItemButton, EuiIcon } from '@elastic/eui'; import React, { useMemo } from 'react'; import { CoreStart } from '../../..'; - import { isModifiedOrPrevented } from './nav_link'; + +/** + * This component is used for application to render top right navigation button in header. + */ + export interface RightNavigationButtonProps { application: CoreStart['application']; http: CoreStart['http']; @@ -16,6 +20,9 @@ export interface RightNavigationButtonProps { title: string; } +/** + * @experimental this class is experimental and might change in future releases. + */ export const RightNavigationButton = ({ application, http, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index dce39d03da7f..af9883000f34 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -80,6 +80,7 @@ export function pluginInitializerContextConfigMock(config: T) { configIndex: '.opensearch_dashboards_config_tests', autocompleteTerminateAfter: duration(100000), autocompleteTimeout: duration(1000), + futureNavigation: false, }, opensearch: { shardTimeout: duration('30s'), diff --git a/src/core/server/opensearch_dashboards_config.ts b/src/core/server/opensearch_dashboards_config.ts index 47fa8a126501..68973f3dd648 100644 --- a/src/core/server/opensearch_dashboards_config.ts +++ b/src/core/server/opensearch_dashboards_config.ts @@ -91,6 +91,7 @@ export const config = { defaultValue: 'https://survey.opensearch.org', }), }), + futureNavigation: schema.boolean({ defaultValue: false }), }), deprecations, }; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 57aa372514de..5616ec8b3b28 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -101,6 +101,7 @@ describe('createPluginInitializerContext', () => { configIndex: '.opensearch_dashboards_config', autocompleteTerminateAfter: duration(100000), autocompleteTimeout: duration(1000), + futureNavigation: false, }, opensearch: { shardTimeout: duration(30, 's'), diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index c225a24aa386..4a0f095bd58f 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -292,6 +292,7 @@ export const SharedGlobalConfigKeys = { 'configIndex', 'autocompleteTerminateAfter', 'autocompleteTimeout', + 'futureNavigation', ] as const, opensearch: ['shardTimeout', 'requestTimeout', 'pingTimeout'] as const, path: ['data'] as const, diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index a102268effca..f109c6058662 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -251,6 +251,7 @@ export default () => survey: Joi.object({ url: Joi.any().default('/'), }), + futureNavigation: Joi.boolean().default(false), }).default(), savedObjects: HANDLED_IN_NEW_PLATFORM, diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index fa89863198cb..64db4fb357de 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -51,10 +51,15 @@ export class ConsoleServerPlugin implements Plugin { } async setup({ http, capabilities, opensearch, security }: CoreSetup) { + const config = await this.ctx.config.create().pipe(first()).toPromise(); + const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); + const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); + capabilities.registerProvider(() => ({ dev_tools: { show: true, save: true, + futureNavigation: globalConfig.opensearchDashboards.futureNavigation, }, })); @@ -66,10 +71,6 @@ export class ConsoleServerPlugin implements Plugin { }); }); - const config = await this.ctx.config.create().pipe(first()).toPromise(); - const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); - const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); - this.opensearchLegacyConfigService.setup(opensearch.legacy.config$); const router = http.createRouter(); diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index e2f3d0cf45e3..6bc40adc5d54 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -138,19 +138,22 @@ export class DevToolsPlugin implements Plugin { if (this.getSortedDevTools().length === 0) { this.appStateUpdater.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden })); } else { - // Register right navigation for dev tool only when console is enabled. - core.chrome.navControls.registerRight({ - order: RightNavigationOrder.DevTool, - mount: toMountPoint( - React.createElement(RightNavigationButton, { - appId: this.id, - iconType: 'consoleApp', - title: this.title, - application: core.application, - http: core.http, - }) - ), - }); + // Register right navigation for dev tool only when console and futureNavigation are both enabled. + const topRightNavigationEnabled = core.application.capabilities?.dev_tools?.futureNavigation; + if (topRightNavigationEnabled) { + core.chrome.navControls.registerRight({ + order: RightNavigationOrder.DevTool, + mount: toMountPoint( + React.createElement(RightNavigationButton, { + appId: this.id, + iconType: 'consoleApp', + title: this.title, + application: core.application, + http: core.http, + }) + ), + }); + } } } From 620d8521bbada22db55836b23610351e3bb12825 Mon Sep 17 00:00:00 2001 From: yujin-emma Date: Tue, 7 May 2024 20:44:10 +0000 Subject: [PATCH 09/27] move the css to the data source aggregated view Signed-off-by: yujin-emma --- .../data_source_aggregated_view.scss | 18 ++++++++++++++++++ .../data_source_item/data_source_item.scss | 17 ----------------- .../data_source_item/data_source_item.tsx | 1 - 3 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss index 15aa1a8c9fcc..2b086dc1aced 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss @@ -47,4 +47,22 @@ align-items: center; justify-content: center; } + + .dataSourceAggregatedViewOuiFlexItem { + color: grey; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + padding-left: 0%; + } + + .dataSourceListAllActiveOuiFlexItem { + color: grey; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + padding-left: 1em; + } } diff --git a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss deleted file mode 100644 index 66149e36bbc0..000000000000 --- a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.scss +++ /dev/null @@ -1,17 +0,0 @@ -.dataSourceAggregatedViewOuiFlexItem { - color: grey; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - display: inline-block; - padding-left: 0%; -} - -.dataSourceListAllActiveOuiFlexItem { - color: grey; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - display: inline-block; - padding-left: 1em; -} diff --git a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx index 47af64c99988..06064ccd1836 100644 --- a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx +++ b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.tsx @@ -5,7 +5,6 @@ import React from 'react'; import { EuiBadge, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { DataSourceOption } from '../data_source_menu/types'; -import './data_source_item.scss'; interface DataSourceItemProps { className: string; From 077298fc6c7a4541b6488a993a7aa6da75a5a8f1 Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Tue, 7 May 2024 15:09:59 -0700 Subject: [PATCH 10/27] [Multiple Datasource Test]add more test for icon and aggregated view (#6729) * add more test for icon and aggregated view Signed-off-by: yujin-emma * Changeset file for PR #6729 created/updated * Update CHANGELOG.md --------- Signed-off-by: yujin-emma Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6729.yml | 2 + .../__snapshots__/empty_icon.test.tsx.snap | 39 + .../__snapshots__/error_icon.test.tsx.snap | 24 + .../custom_database_icon/empty_icon.test.tsx | 15 + .../custom_database_icon/error_icon.test.tsx | 15 + .../data_source_aggregated_view.test.tsx.snap | 950 ++++++++++++++++++ .../data_source_aggregated_view.test.tsx | 233 +++++ 7 files changed, 1278 insertions(+) create mode 100644 changelogs/fragments/6729.yml create mode 100644 src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/empty_icon.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/error_icon.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.test.tsx create mode 100644 src/plugins/data_source_management/public/components/custom_database_icon/error_icon.test.tsx diff --git a/changelogs/fragments/6729.yml b/changelogs/fragments/6729.yml new file mode 100644 index 000000000000..8a564e684ffc --- /dev/null +++ b/changelogs/fragments/6729.yml @@ -0,0 +1,2 @@ +fix: +- Add more test for icon and aggregated view ([#6729](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6729)) \ No newline at end of file diff --git a/src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/empty_icon.test.tsx.snap b/src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/empty_icon.test.tsx.snap new file mode 100644 index 000000000000..28f9cf9d4c68 --- /dev/null +++ b/src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/empty_icon.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test on empty icon should render the component normally 1`] = ` + + + + + + + + +`; diff --git a/src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/error_icon.test.tsx.snap b/src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/error_icon.test.tsx.snap new file mode 100644 index 000000000000..fcb1050538a8 --- /dev/null +++ b/src/plugins/data_source_management/public/components/custom_database_icon/__snapshots__/error_icon.test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test on empty icon should render the component normally 1`] = ` + + + + +`; diff --git a/src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.test.tsx b/src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.test.tsx new file mode 100644 index 000000000000..e72c429ec391 --- /dev/null +++ b/src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.test.tsx @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { EmptyIcon } from './empty_icon'; + +describe('Test on empty icon', () => { + it('should render the component normally', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data_source_management/public/components/custom_database_icon/error_icon.test.tsx b/src/plugins/data_source_management/public/components/custom_database_icon/error_icon.test.tsx new file mode 100644 index 000000000000..bba81a16f071 --- /dev/null +++ b/src/plugins/data_source_management/public/components/custom_database_icon/error_icon.test.tsx @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { ErrorIcon } from './error_icon'; + +describe('Test on empty icon', () => { + it('should render the component normally', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap index f6324c17061d..23fee253ce08 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap @@ -1,5 +1,955 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`DataSourceAggregatedView empty state test due to filter out with local cluster hiding should render warning when no data sources added 1`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView empty state test due to filter out with local cluster hiding should render warning when no data sources added 2`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView empty state test with local cluster hiding should render warning when no data sources added 1`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView empty state test with local cluster hiding should render warning when no data sources added 2`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView empty state test with local cluster hiding should render warning when no data sources added 3`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView empty state test with local cluster hiding should render warning when no data sources added 4`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView error state test no matter hide local cluster or not should render error state when catch error 1`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView error state test no matter hide local cluster or not should render error state when catch error 2`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView error state test no matter hide local cluster or not should render error state when catch error 3`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView error state test no matter hide local cluster or not should render error state when catch error 4`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + + + + +`; + exports[`DataSourceAggregatedView: read active view (displayAllCompatibleDataSources is set to false) should render normally with local cluster and active selections configured 1`] = ` { + let component: ShallowWrapper, React.Component<{}, {}, any>>; + let client: SavedObjectsClientContract; + const { toasts } = notificationServiceMock.createStartContract(); + const uiSettings = uiSettingsServiceMock.createStartContract(); + const application = applicationServiceMock.createStartContract(); + const nextTick = () => new Promise((res) => process.nextTick(res)); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + mockResponseForSavedObjectsCalls(client, 'find', {}); + mockUiSettingsCalls(uiSettings, 'get', 'test1'); + jest.spyOn(utils, 'getApplication').mockReturnValue(application); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear all mocks to reset call counts and mock implementations + }); + + it.each([ + { + filter: (ds: SavedObject) => { + return true; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: true, + }, + { + filter: (ds: SavedObject) => { + return false; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: true, + }, + { + filter: (ds: SavedObject) => { + return true; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: false, + }, + { + filter: (ds: SavedObject) => { + return false; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: false, + }, + ])( + 'should render warning when no data sources added', + async ({ filter, activeDataSourceIds, hideLocalCluster, displayAllCompatibleDataSources }) => { + component = shallow( + + ); + + expect(component).toMatchSnapshot(); + await nextTick(); + expect(toasts.add).toHaveBeenCalledTimes(1); + expect(toasts.add.mock.calls[0][0]).toEqual({ + color: 'warning', + text: expect.any(Function), + title: 'No data sources connected yet. Connect your data sources to get started.', + }); + expect(component.state('showEmptyState')).toBe(true); + await nextTick(); + expect(component.find('NoDataSource').exists()).toBe(true); + } + ); +}); + +describe('DataSourceAggregatedView empty state test due to filter out with local cluster hiding', () => { + let component: ShallowWrapper, React.Component<{}, {}, any>>; + let client: SavedObjectsClientContract; + const { toasts } = notificationServiceMock.createStartContract(); + const uiSettings = uiSettingsServiceMock.createStartContract(); + const application = applicationServiceMock.createStartContract(); + const nextTick = () => new Promise((res) => process.nextTick(res)); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse); + mockUiSettingsCalls(uiSettings, 'get', 'test1'); + jest.spyOn(utils, 'getApplication').mockReturnValue(application); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear all mocks to reset call counts and mock implementations + }); + + it.each([ + { + filter: (ds: SavedObject) => { + return false; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: true, + }, + { + filter: (ds: SavedObject) => { + return false; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: false, + }, + ])( + 'should render warning when no data sources added', + async ({ filter, activeDataSourceIds, hideLocalCluster, displayAllCompatibleDataSources }) => { + component = shallow( + + ); + + expect(component).toMatchSnapshot(); + await nextTick(); + expect(toasts.add).toHaveBeenCalledTimes(1); + expect(toasts.add.mock.calls[0][0]).toEqual({ + color: 'warning', + text: expect.any(Function), + title: 'No data sources connected yet. Connect your data sources to get started.', + }); + expect(component.state('showEmptyState')).toBe(true); + await nextTick(); + expect(component.find('NoDataSource').exists()).toBe(true); + } + ); +}); + +describe('DataSourceAggregatedView error state test no matter hide local cluster or not', () => { + let component: ShallowWrapper, React.Component<{}, {}, any>>; + let client: SavedObjectsClientContract; + const { toasts } = notificationServiceMock.createStartContract(); + const uiSettings = uiSettingsServiceMock.createStartContract(); + const application = applicationServiceMock.createStartContract(); + const nextTick = () => new Promise((res) => process.nextTick(res)); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + mockErrorResponseForSavedObjectsCalls(client, 'find'); + mockUiSettingsCalls(uiSettings, 'get', 'test1'); + jest.spyOn(utils, 'getApplication').mockReturnValue(application); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear all mocks to reset call counts and mock implementations + }); + + it.each([ + { + filter: (ds: SavedObject) => { + return true; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: true, + }, + { + filter: (ds: SavedObject) => { + return false; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: true, + }, + { + filter: (ds: SavedObject) => { + return true; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: false, + }, + { + filter: (ds: SavedObject) => { + return false; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + displayAllCompatibleDataSources: false, + }, + ])( + 'should render error state when catch error', + async ({ filter, activeDataSourceIds, hideLocalCluster, displayAllCompatibleDataSources }) => { + component = shallow( + + ); + + expect(component).toMatchSnapshot(); + await nextTick(); + expect(toasts.add).toBeCalled(); + expect(component.state('showError')).toBe(true); + } + ); +}); From d435cca0e270fade28dc71634fb3f294050b9d70 Mon Sep 17 00:00:00 2001 From: yujin-emma Date: Wed, 8 May 2024 20:46:48 +0000 Subject: [PATCH 11/27] update css file space and use the oui color guideline number to replace the color name in css file Signed-off-by: yujin-emma --- .../data_source_aggregated_view.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss index 2b086dc1aced..da22ed7e2bde 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss @@ -21,8 +21,6 @@ .euiSelectableListItem__icon, .euiSelectableListItem__prepend { margin-right: 0; } - - } .dataSourceAggregatedViewOuiFlexGroup { @@ -38,7 +36,7 @@ overflow: hidden; white-space: nowrap; display: inline-block; - padding-left: 0%; + padding-left: 0; } } @@ -54,7 +52,7 @@ overflow: hidden; white-space: nowrap; display: inline-block; - padding-left: 0%; + padding-left: 0; } .dataSourceListAllActiveOuiFlexItem { From e4b74a1221ff89d8a9e0ecd79057f72ee5435e83 Mon Sep 17 00:00:00 2001 From: Tao Liu <33105471+Flyingliuhub@users.noreply.github.com> Date: Tue, 7 May 2024 18:10:37 -0700 Subject: [PATCH 12/27] [OSD Availability] Prevent OSD process crashes when disk is full (#6733) * prevent crash when disk full Signed-off-by: Flyingliuhub <33105471+flyingliuhub@users.noreply.github.com> * change verbose to false Signed-off-by: Flyingliuhub <33105471+flyingliuhub@users.noreply.github.com> * add changeset file Signed-off-by: Flyingliuhub <33105471+flyingliuhub@users.noreply.github.com> * update changeset contexts Signed-off-by: Flyingliuhub <33105471+flyingliuhub@users.noreply.github.com> * change feature flag name Signed-off-by: Flyingliuhub <33105471+flyingliuhub@users.noreply.github.com> --------- Signed-off-by: Flyingliuhub <33105471+flyingliuhub@users.noreply.github.com> Co-authored-by: ZilongX <99905560+ZilongX@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6733.yml | 2 + config/opensearch_dashboards.yml | 5 + .../bin/opensearch-dashboards-docker | 1 + src/legacy/server/config/schema.js | 1 + src/legacy/server/logging/configuration.js | 1 + src/legacy/server/logging/log_reporter.js | 26 ++- .../server/logging/log_reporter.test.js | 148 ++++++++++++++++++ 7 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 changelogs/fragments/6733.yml create mode 100644 src/legacy/server/logging/log_reporter.test.js diff --git a/changelogs/fragments/6733.yml b/changelogs/fragments/6733.yml new file mode 100644 index 000000000000..2021bce6c9dc --- /dev/null +++ b/changelogs/fragments/6733.yml @@ -0,0 +1,2 @@ +fix: +- [OSD Availability] Prevent OSD process crashes when disk is full ([#6733](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6733)) \ No newline at end of file diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index f94234cffe1b..0844db34c36c 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -135,6 +135,11 @@ # Enables you to specify a file where OpenSearch Dashboards stores log output. #logging.dest: stdout +# This configuration option controls the handling of error messages in the logging stream. It is disabled by default. +# When set to true, the 'ENOSPC' error message will not cause the OpenSearch Dashboards process to crash. Otherwise, +# the original behavior will be maintained. +#logging.ignoreEnospcError: false + # Set the value of this setting to true to suppress all logging output. #logging.silent: false diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker index c9cf5d1213c0..3f17d5e7cc60 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker @@ -58,6 +58,7 @@ opensearch_dashboards_vars=( opensearchDashboards.defaultAppId opensearchDashboards.index logging.dest + logging.ignoreEnospcError logging.json logging.quiet logging.rotate.enabled diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index f109c6058662..4c8d5c2bce6c 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -109,6 +109,7 @@ export default () => }), events: Joi.any().default({}), dest: Joi.string().default('stdout'), + ignoreEnospcError: Joi.boolean().default(false), filter: Joi.any().default({}), json: Joi.boolean().when('dest', { is: 'stdout', diff --git a/src/legacy/server/logging/configuration.js b/src/legacy/server/logging/configuration.js index 93103b3e5067..e942af2b9352 100644 --- a/src/legacy/server/logging/configuration.js +++ b/src/legacy/server/logging/configuration.js @@ -64,6 +64,7 @@ export default function loggingConfiguration(config) { json: config.get('logging.json'), dest: config.get('logging.dest'), timezone: config.get('logging.timezone'), + ignoreEnospcError: config.get('logging.ignoreEnospcError'), // I'm adding the default here because if you add another filter // using the commandline it will remove authorization. I want users diff --git a/src/legacy/server/logging/log_reporter.js b/src/legacy/server/logging/log_reporter.js index 228c83129802..b8e39304a7b6 100644 --- a/src/legacy/server/logging/log_reporter.js +++ b/src/legacy/server/logging/log_reporter.js @@ -30,6 +30,7 @@ import { Squeeze } from '@hapi/good-squeeze'; import { createWriteStream as writeStr } from 'fs'; +import { pipeline } from 'stream'; import LogFormatJson from './log_format_json'; import LogFormatString from './log_format_string'; @@ -51,18 +52,33 @@ export function getLoggerStream({ events, config }) { let dest; if (config.dest === 'stdout') { dest = process.stdout; + logInterceptor.pipe(squeeze).pipe(format).pipe(dest); } else { dest = writeStr(config.dest, { flags: 'a', encoding: 'utf8', }); - logInterceptor.on('end', () => { - dest.end(); - }); + if (config.ignoreEnospcError) { + pipeline(logInterceptor, squeeze, format, dest, onFinished); + } else { + logInterceptor.on('end', () => { + dest.end(); + }); + logInterceptor.pipe(squeeze).pipe(format).pipe(dest); + } } - logInterceptor.pipe(squeeze).pipe(format).pipe(dest); - return logInterceptor; } + +export function onFinished(error) { + if (error) { + if (error.code === 'ENOSPC') { + // eslint-disable-next-line no-console + console.error('Error in logging pipeline:', error.stack); + } else { + throw error; + } + } +} diff --git a/src/legacy/server/logging/log_reporter.test.js b/src/legacy/server/logging/log_reporter.test.js new file mode 100644 index 000000000000..babe5b7e6858 --- /dev/null +++ b/src/legacy/server/logging/log_reporter.test.js @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import stripAnsi from 'strip-ansi'; +import { getLoggerStream, onFinished } from './log_reporter'; + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('getLoggerStream', () => { + it('should log to stdout when the json config is set to false', async () => { + const lines = []; + const origWrite = process.stdout.write; + process.stdout.write = (buffer) => { + lines.push(stripAnsi(buffer.toString()).trim()); + return true; + }; + + const loggerStream = getLoggerStream({ + config: { + json: false, + dest: 'stdout', + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'test data' }); + + await sleep(500); + + process.stdout.write = origWrite; + expect(lines.length).toBe(1); + expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] test data$/); + }); + + it('should log to stdout when the json config is set to true', async () => { + const lines = []; + const origWrite = process.stdout.write; + process.stdout.write = (buffer) => { + lines.push(JSON.parse(buffer.toString().trim())); + return true; + }; + + const loggerStream = getLoggerStream({ + config: { + json: true, + dest: 'stdout', + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'test data' }); + + await sleep(500); + + process.stdout.write = origWrite; + expect(lines.length).toBe(1); + expect(lines[0]).toMatchObject({ + type: 'log', + tags: ['foo'], + message: 'test data', + }); + }); + + it('should log to custom file when the json config is set to false', async () => { + const dir = os.tmpdir(); + const logfile = `dest-${Date.now()}.log`; + const dest = path.join(dir, logfile); + + const loggerStream = getLoggerStream({ + config: { + json: false, + dest, + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'test data' }); + + await sleep(500); + + const lines = stripAnsi(fs.readFileSync(dest, { encoding: 'utf8' })) + .trim() + .split(os.EOL); + expect(lines.length).toBe(1); + expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] test data$/); + }); + + it('should log to custom file when the json config is set to true and ignoreEnospcError', async () => { + const dir = os.tmpdir(); + const logfile = `dest-${Date.now()}.log`; + const dest = path.join(dir, logfile); + + const loggerStream = getLoggerStream({ + config: { + json: true, + dest, + ignoreEnospcError: true, + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'test data' }); + + await sleep(500); + + const lines = fs + .readFileSync(dest, { encoding: 'utf8' }) + .trim() + .split(os.EOL) + .map((data) => JSON.parse(data)); + expect(lines.length).toBe(1); + expect(lines[0]).toMatchObject({ + type: 'log', + tags: ['foo'], + message: 'test data', + }); + }); + + it('should handle ENOSPC error when disk full', () => { + const error = { code: 'ENOSPC', stack: 'Error stack trace' }; + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + expect(() => { + onFinished(error); + }).not.toThrow(); + + expect(consoleErrorSpy).toHaveBeenCalledWith('Error in logging pipeline:', 'Error stack trace'); + + consoleErrorSpy.mockRestore(); + }); + + it('should throw error for non-ENOSPC error', () => { + const error = { message: 'non-ENOSPC error', code: 'OTHER', stack: 'Error stack trace' }; + + expect(() => { + onFinished(error); + }).toThrowError('non-ENOSPC error'); + }); +}); From 49ae21fd312325a1cac1530e64128247f3e0543d Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Wed, 8 May 2024 10:44:58 -0700 Subject: [PATCH 13/27] [MDS] Modify toast + popover warning to include incompatible datasources (#6678) * Fix merge conflict Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Refactor to incompatibleDataSourcesExist Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> * Changeset file for PR #6678 created/updated * Move required args to the top Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --------- Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6678.yml | 2 + .../public/components/constants.tsx | 5 ++ .../data_source_aggregated_view.test.tsx | 73 +++++++++++++++- .../data_source_aggregated_view.tsx | 24 ++++-- .../data_source_multi_selectable.tsx | 19 +++-- .../data_source_selectable.test.tsx | 81 ++++++++++++++---- .../data_source_selectable.tsx | 28 +++--- .../no_data_source.test.tsx.snap | 85 ++++++++++++++++++- .../no_data_source/no_data_source.test.tsx | 32 +++++-- .../no_data_source/no_data_source.tsx | 24 +++++- .../public/components/utils.test.ts | 48 +++++++++-- .../public/components/utils.ts | 31 +++++-- 12 files changed, 379 insertions(+), 73 deletions(-) create mode 100644 changelogs/fragments/6678.yml diff --git a/changelogs/fragments/6678.yml b/changelogs/fragments/6678.yml new file mode 100644 index 000000000000..46e9568f5653 --- /dev/null +++ b/changelogs/fragments/6678.yml @@ -0,0 +1,2 @@ +fix: +- [MDS] Add a new message to data source components when there are no compatible datasources ([#6678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6678)) \ No newline at end of file diff --git a/src/plugins/data_source_management/public/components/constants.tsx b/src/plugins/data_source_management/public/components/constants.tsx index 0d22aed50179..720e2d175867 100644 --- a/src/plugins/data_source_management/public/components/constants.tsx +++ b/src/plugins/data_source_management/public/components/constants.tsx @@ -12,3 +12,8 @@ export const LocalCluster: DataSourceOption = { }), id: '', }; + +export const NO_DATASOURCES_CONNECTED_MESSAGE = 'No data sources connected yet.'; +export const CONNECT_DATASOURCES_MESSAGE = 'Connect your data sources to get started.'; +export const NO_COMPATIBLE_DATASOURCES_MESSAGE = 'No compatible data sources are available.'; +export const ADD_COMPATIBLE_DATASOURCES_MESSAGE = 'Add a compatible data source.'; diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx index f7493ff843be..43b22ed219ee 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx @@ -5,8 +5,9 @@ import { ShallowWrapper, shallow } from 'enzyme'; import React from 'react'; +import { i18n } from '@osd/i18n'; import { DataSourceAggregatedView } from './data_source_aggregated_view'; -import { SavedObject, SavedObjectsClientContract } from '../../../../../core/public'; +import { IToasts, SavedObject, SavedObjectsClientContract } from '../../../../../core/public'; import { applicationServiceMock, notificationServiceMock, @@ -21,6 +22,12 @@ import { import * as utils from '../utils'; import { EuiSelectable, EuiSwitch } from '@elastic/eui'; import { DataSourceAttributes } from '../../types'; +import { + ADD_COMPATIBLE_DATASOURCES_MESSAGE, + CONNECT_DATASOURCES_MESSAGE, + NO_COMPATIBLE_DATASOURCES_MESSAGE, + NO_DATASOURCES_CONNECTED_MESSAGE, +} from '../constants'; describe('DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true)', () => { let component: ShallowWrapper, React.Component<{}, {}, any>>; @@ -409,6 +416,7 @@ describe('DataSourceAggregatedView empty state test due to filter out with local dataSourceFilter={filter} /> ); + const noCompatibleDataSourcesMessage = `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}`; expect(component).toMatchSnapshot(); await nextTick(); @@ -416,7 +424,7 @@ describe('DataSourceAggregatedView empty state test due to filter out with local expect(toasts.add.mock.calls[0][0]).toEqual({ color: 'warning', text: expect.any(Function), - title: 'No data sources connected yet. Connect your data sources to get started.', + title: noCompatibleDataSourcesMessage, }); expect(component.state('showEmptyState')).toBe(true); await nextTick(); @@ -502,3 +510,64 @@ describe('DataSourceAggregatedView error state test no matter hide local cluster } ); }); + +describe('DataSourceAggregatedView warning messages', () => { + const client = {} as any; + const uiSettings = uiSettingsServiceMock.createStartContract(); + const nextTick = () => new Promise((res) => process.nextTick(res)); + let toasts: IToasts; + const noDataSourcesConnectedMessage = `${NO_DATASOURCES_CONNECTED_MESSAGE} ${CONNECT_DATASOURCES_MESSAGE}`; + const noCompatibleDataSourcesMessage = `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}`; + + beforeEach(() => { + toasts = notificationServiceMock.createStartContract().toasts; + mockUiSettingsCalls(uiSettings, 'get', 'test1'); + }); + + it.each([ + { + findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse), + defaultMessage: noCompatibleDataSourcesMessage, + activeDataSourceIds: ['test2'], + }, + { + findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }), + defaultMessage: noDataSourcesConnectedMessage, + activeDataSourceIds: ['test2'], + }, + { + findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse), + defaultMessage: noCompatibleDataSourcesMessage, + activeDataSourceIds: undefined, + }, + { + findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }), + defaultMessage: noDataSourcesConnectedMessage, + activeDataSourceIds: undefined, + }, + ])( + 'should display correct warning message when no datasource selections are available and local cluster is hidden', + async ({ findFunc, defaultMessage, activeDataSourceIds }) => { + client.find = findFunc; + shallow( + false} + uiSettings={uiSettings} + /> + ); + await nextTick(); + + expect(toasts.add).toBeCalledWith( + expect.objectContaining({ + title: i18n.translate('dataSource.noAvailableDataSourceError', { defaultMessage }), + }) + ); + } + ); +}); 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 caaaac665a8d..1d48965fa041 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 @@ -45,6 +45,7 @@ interface DataSourceAggregatedViewState extends DataSourceBaseState { allDataSourcesIdToTitleMap: Map; switchChecked: boolean; defaultDataSource: string | null; + incompatibleDataSourcesExist: boolean; } interface DataSourceOptionDisplay extends DataSourceOption { @@ -68,6 +69,7 @@ export class DataSourceAggregatedView extends React.Component< showError: false, switchChecked: false, defaultDataSource: null, + incompatibleDataSourcesExist: false, }; } @@ -113,11 +115,12 @@ export class DataSourceAggregatedView extends React.Component< } if (allDataSourcesIdToTitleMap.size === 0) { - handleNoAvailableDataSourceError( - this.onEmptyState.bind(this), - this.props.notifications, - this.props.application - ); + handleNoAvailableDataSourceError({ + changeState: this.onEmptyState.bind(this, !!fetchedDataSources?.length), + notifications: this.props.notifications, + application: this.props.application, + incompatibleDataSourcesExist: !!fetchedDataSources?.length, + }); return; } @@ -133,8 +136,8 @@ export class DataSourceAggregatedView extends React.Component< }); } - onEmptyState() { - this.setState({ showEmptyState: true }); + onEmptyState(incompatibleDataSourcesExist: boolean) { + this.setState({ showEmptyState: true, incompatibleDataSourcesExist }); } onError() { @@ -143,7 +146,12 @@ export class DataSourceAggregatedView extends React.Component< render() { if (this.state.showEmptyState) { - return ; + return ( + + ); } if (this.state.showError) { return ; 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 85506ec84b61..481df093d741 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 @@ -34,6 +34,7 @@ interface DataSourceMultiSeletableState extends DataSourceBaseState { dataSourceOptions: SelectedDataSourceOption[]; selectedOptions: SelectedDataSourceOption[]; defaultDataSource: string | null; + incompatibleDataSourcesExist: boolean; } export class DataSourceMultiSelectable extends React.Component< @@ -51,6 +52,7 @@ export class DataSourceMultiSelectable extends React.Component< defaultDataSource: null, showEmptyState: false, showError: false, + incompatibleDataSourcesExist: false, }; } @@ -90,12 +92,13 @@ export class DataSourceMultiSelectable extends React.Component< if (!this._isMounted) return; if (selectedOptions.length === 0) { - handleNoAvailableDataSourceError( - this.onEmptyState.bind(this), - this.props.notifications, - this.props.application, - this.props.onSelectedDataSources - ); + handleNoAvailableDataSourceError({ + changeState: this.onEmptyState.bind(this, !!fetchedDataSources?.length), + notifications: this.props.notifications, + application: this.props.application, + callback: this.props.onSelectedDataSources, + incompatibleDataSourcesExist: !!fetchedDataSources?.length, + }); return; } @@ -115,8 +118,8 @@ export class DataSourceMultiSelectable extends React.Component< } } - onEmptyState() { - this.setState({ showEmptyState: true }); + onEmptyState(incompatibleDataSourcesExist: boolean) { + this.setState({ showEmptyState: true, incompatibleDataSourcesExist }); } onError() { diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx index f1cef722c3c1..6521aaddb258 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx @@ -4,6 +4,7 @@ */ import { ShallowWrapper, shallow, mount } from 'enzyme'; +import { i18n } from '@osd/i18n'; import { SavedObjectsClientContract } from '../../../../../core/public'; import { notificationServiceMock } from '../../../../../core/public/mocks'; import React from 'react'; @@ -12,6 +13,12 @@ import { AuthType } from '../../types'; import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks'; import { render } from '@testing-library/react'; import * as utils from '../utils'; +import { + NO_DATASOURCES_CONNECTED_MESSAGE, + CONNECT_DATASOURCES_MESSAGE, + NO_COMPATIBLE_DATASOURCES_MESSAGE, + ADD_COMPATIBLE_DATASOURCES_MESSAGE, +} from '../constants'; describe('DataSourceSelectable', () => { let component: ShallowWrapper, React.Component<{}, {}, any>>; @@ -19,6 +26,8 @@ describe('DataSourceSelectable', () => { let client: SavedObjectsClientContract; const { toasts } = notificationServiceMock.createStartContract(); const nextTick = () => new Promise((res) => process.nextTick(res)); + const noDataSourcesConnectedMessage = `${NO_DATASOURCES_CONNECTED_MESSAGE} ${CONNECT_DATASOURCES_MESSAGE}`; + const noCompatibleDataSourcesMessage = `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}`; beforeEach(() => { client = { @@ -145,6 +154,7 @@ describe('DataSourceSelectable', () => { }, ], showError: false, + incompatibleDataSourcesExist: false, }); containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]); @@ -167,6 +177,7 @@ describe('DataSourceSelectable', () => { }, ], showError: false, + incompatibleDataSourcesExist: false, }); expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]); @@ -345,6 +356,7 @@ describe('DataSourceSelectable', () => { }, ], showError: false, + incompatibleDataSourcesExist: false, }); }); @@ -374,6 +386,7 @@ describe('DataSourceSelectable', () => { selectedOption: [], showEmptyState: false, showError: true, + incompatibleDataSourcesExist: false, }); containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]); @@ -396,27 +409,59 @@ describe('DataSourceSelectable', () => { }, ], showError: true, + incompatibleDataSourcesExist: false, }); expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]); expect(onSelectedDataSource).toHaveBeenCalled(); }); - it('should render no data source when no data source filtered out and hide local cluster', async () => { - const onSelectedDataSource = jest.fn(); - render( - false} - /> - ); - await nextTick(); - expect(toasts.add).toBeCalled(); - expect(onSelectedDataSource).toBeCalledWith([]); - }); + + it.each([ + { + findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }), + defaultMessage: noDataSourcesConnectedMessage, + selectedOption: undefined, + }, + { + findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }), + defaultMessage: noDataSourcesConnectedMessage, + selectedOption: [{ id: 'test2' }], + }, + { + findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse), + defaultMessage: noCompatibleDataSourcesMessage, + selectedOption: undefined, + }, + { + findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse), + defaultMessage: noCompatibleDataSourcesMessage, + selectedOption: [{ id: 'test2' }], + }, + ])( + 'should render correct message when there are no datasource options available and local cluster is hidden', + async ({ findFunc, selectedOption, defaultMessage }) => { + client.find = findFunc; + const onSelectedDataSource = jest.fn(); + render( + false} + /> + ); + await nextTick(); + + expect(toasts.add).toBeCalledWith( + expect.objectContaining({ + title: i18n.translate('dataSource.noAvailableDataSourceError', { defaultMessage }), + }) + ); + expect(onSelectedDataSource).toBeCalledWith([]); + } + ); }); 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 63b9eebf26c1..fd1c685676f0 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 @@ -54,8 +54,9 @@ interface DataSourceSelectableProps { interface DataSourceSelectableState extends DataSourceBaseState { dataSourceOptions: DataSourceOption[]; isPopoverOpen: boolean; - selectedOption?: DataSourceOption[]; defaultDataSource: string | null; + incompatibleDataSourcesExist: boolean; + selectedOption?: DataSourceOption[]; } export class DataSourceSelectable extends React.Component< @@ -74,6 +75,7 @@ export class DataSourceSelectable extends React.Component< defaultDataSource: null, showEmptyState: false, showError: false, + incompatibleDataSourcesExist: false, }; this.onChange.bind(this); @@ -187,12 +189,13 @@ export class DataSourceSelectable extends React.Component< } if (dataSourceOptions.length === 0) { - handleNoAvailableDataSourceError( - this.onEmptyState.bind(this), - this.props.notifications, - this.props.application, - this.props.onSelectedDataSources - ); + handleNoAvailableDataSourceError({ + changeState: this.onEmptyState.bind(this, !!fetchedDataSources?.length), + notifications: this.props.notifications, + application: this.props.application, + callback: this.props.onSelectedDataSources, + incompatibleDataSourcesExist: !!fetchedDataSources?.length, + }); return; } @@ -214,8 +217,8 @@ export class DataSourceSelectable extends React.Component< } } - onEmptyState() { - this.setState({ showEmptyState: true }); + onEmptyState(incompatibleDataSourcesExist: boolean) { + this.setState({ showEmptyState: true, incompatibleDataSourcesExist }); } onError() { @@ -242,7 +245,12 @@ export class DataSourceSelectable extends React.Component< render() { if (this.state.showEmptyState) { - return ; + return ( + + ); } if (this.state.showError) { diff --git a/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap b/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap index ee8f2120012f..f7f3e41b9cb5 100644 --- a/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap @@ -83,7 +83,90 @@ exports[`NoDataSource should render correctly with the provided totalDataSourceC `; -exports[`NoDataSource should render normally 1`] = ` +exports[`NoDataSource should render normally when incompatibleDataSourcesExist is %b 1`] = ` + + } + closePopover={[Function]} + data-test-subj="dataSourceEmptyStatePopover" + display="inlineBlock" + hasArrow={true} + id="dataSourceEmptyStatePopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`NoDataSource should render normally when incompatibleDataSourcesExist is %b 2`] = ` { const nextTick = () => new Promise((res) => process.nextTick(res)); it('should render correctly with the provided totalDataSourceCount', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); it('should display popover when click "No data sources" button', async () => { const applicationMock = coreMock.createStart().application; const container = render( - + ); await nextTick(); @@ -39,7 +45,11 @@ describe('NoDataSource', () => { const navigateToAppMock = applicationMock.navigateToApp; const container = render( - + ); await nextTick(); @@ -55,8 +65,16 @@ describe('NoDataSource', () => { }); }); - it('should render normally', () => { - component = shallow(); - expect(component).toMatchSnapshot(); - }); + it.each([false, true])( + 'should render normally when incompatibleDataSourcesExist is %b', + (incompatibleDataSourcesExist) => { + component = shallow( + + ); + expect(component).toMatchSnapshot(); + } + ); }); diff --git a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx index d10efe8c4a7b..6b48a7a7a0b4 100644 --- a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx +++ b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx @@ -20,12 +20,22 @@ import { FormattedMessage } from 'react-intl'; import { DataSourceDropDownHeader } from '../drop_down_header'; import { DSM_APP_ID } from '../../plugin'; import { EmptyIcon } from '../custom_database_icon'; +import { + ADD_COMPATIBLE_DATASOURCES_MESSAGE, + CONNECT_DATASOURCES_MESSAGE, + NO_COMPATIBLE_DATASOURCES_MESSAGE, + NO_DATASOURCES_CONNECTED_MESSAGE, +} from '../constants'; interface DataSourceDropDownHeaderProps { + incompatibleDataSourcesExist: boolean; application?: ApplicationStart; } -export const NoDataSource: React.FC = ({ application }) => { +export const NoDataSource: React.FC = ({ + application, + incompatibleDataSourcesExist, +}) => { const [showPopover, setShowPopover] = useState(false); const button = ( = ({ applicat { } @@ -72,7 +86,11 @@ export const NoDataSource: React.FC = ({ applicat { } 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 b2628e3d3062..7e842345812e 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -42,10 +42,17 @@ import { sigV4AuthMethod, usernamePasswordAuthMethod, } from '../types'; -import { HttpStart, SavedObject } from 'opensearch-dashboards/public'; +import { HttpStart, IToasts, SavedObject } from 'opensearch-dashboards/public'; +import { i18n } from '@osd/i18n'; import { AuthenticationMethod, AuthenticationMethodRegistry } from '../auth_registry'; import { deepEqual } from 'assert'; import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources'; +import { + ADD_COMPATIBLE_DATASOURCES_MESSAGE, + CONNECT_DATASOURCES_MESSAGE, + NO_COMPATIBLE_DATASOURCES_MESSAGE, + NO_DATASOURCES_CONNECTED_MESSAGE, +} from './constants'; const { savedObjects } = coreMock.createStart(); const { uiSettings } = coreMock.createStart(); @@ -84,13 +91,40 @@ describe('DataSourceManagement: Utils.ts', () => { }); describe('Handle no available data source error', () => { - const { toasts } = notificationServiceMock.createStartContract(); + let toasts: IToasts; + const noDataSourcesConnectedMessage = `${NO_DATASOURCES_CONNECTED_MESSAGE} ${CONNECT_DATASOURCES_MESSAGE}`; + const noCompatibleDataSourcesMessage = `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}`; - test('should send warning when data source is not available', () => { - const changeState = jest.fn(); - handleNoAvailableDataSourceError(changeState, toasts); - expect(toasts.add).toBeCalledTimes(1); - }); + beforeEach(() => { + toasts = notificationServiceMock.createStartContract().toasts; + }); + + test.each([ + { + incompatibleDataSourcesExist: false, + defaultMessage: noDataSourcesConnectedMessage, + }, + { + incompatibleDataSourcesExist: true, + defaultMessage: noCompatibleDataSourcesMessage, + }, + ])( + 'should send warning when data source is not available', + ({ incompatibleDataSourcesExist, defaultMessage }) => { + const changeState = jest.fn(); + handleNoAvailableDataSourceError({ + changeState, + notifications: toasts, + incompatibleDataSourcesExist, + }); + expect(toasts.add).toBeCalledTimes(1); + expect(toasts.add).toBeCalledWith( + expect.objectContaining({ + title: i18n.translate('dataSource.noAvailableDataSourceError', { defaultMessage }), + }) + ); + } + ); }); describe('Get data source by ID', () => { diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 8f635f840aec..7a4ec06d5cfb 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -25,6 +25,12 @@ import { DataSourceGroupLabelOption } from './data_source_menu/types'; import { createGetterSetter } from '../../../opensearch_dashboards_utils/public'; import { toMountPoint } from '../../../opensearch_dashboards_react/public'; import { getManageDataSourceButton, getReloadButton } from './toast_button'; +import { + ADD_COMPATIBLE_DATASOURCES_MESSAGE, + CONNECT_DATASOURCES_MESSAGE, + NO_COMPATIBLE_DATASOURCES_MESSAGE, + NO_DATASOURCES_CONNECTED_MESSAGE, +} from './constants'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient @@ -87,18 +93,25 @@ export async function setFirstDataSourceAsDefault( } } -export function handleNoAvailableDataSourceError( - changeState: () => void, - notifications: ToastsStart, - application?: ApplicationStart, - callback?: (ds: DataSourceOption[]) => void -) { +export interface HandleNoAvailableDataSourceErrorProps { + changeState: () => void; + notifications: ToastsStart; + incompatibleDataSourcesExist: boolean; + application?: ApplicationStart; + callback?: (ds: DataSourceOption[]) => void; +} + +export function handleNoAvailableDataSourceError(props: HandleNoAvailableDataSourceErrorProps) { + const { changeState, notifications, application, callback, incompatibleDataSourcesExist } = props; + + const defaultMessage = incompatibleDataSourcesExist + ? `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}` + : `${NO_DATASOURCES_CONNECTED_MESSAGE} ${CONNECT_DATASOURCES_MESSAGE}`; + changeState(); if (callback) callback([]); notifications.add({ - title: i18n.translate('dataSource.noAvailableDataSourceError', { - defaultMessage: 'No data sources connected yet. Connect your data sources to get started.', - }), + title: i18n.translate('dataSource.noAvailableDataSourceError', { defaultMessage }), text: toMountPoint(getManageDataSourceButton(application)), color: 'warning', }); From d091efe66017232c6904c6001caa923bcd8934cd Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Thu, 9 May 2024 13:26:19 -0700 Subject: [PATCH 14/27] [Multiple Datasource Test] Add test for edit data source form (#6742) * add test for edit data source form Signed-off-by: yujin-emma * Changeset file for PR #6742 created/updated --------- Signed-off-by: yujin-emma Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6742.yml | 2 + .../edit_form/edit_data_source_form.test.tsx | 194 ++++++++++++++++++ .../edit_form/edit_data_source_form.tsx | 3 + .../update_aws_credential_modal.test.tsx | 67 ++++++ .../data_source_management/public/mocks.ts | 15 ++ 5 files changed, 281 insertions(+) create mode 100644 changelogs/fragments/6742.yml create mode 100644 src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/update_aws_credential_modal.test.tsx diff --git a/changelogs/fragments/6742.yml b/changelogs/fragments/6742.yml new file mode 100644 index 000000000000..390210dff311 --- /dev/null +++ b/changelogs/fragments/6742.yml @@ -0,0 +1,2 @@ +fix: +- Add test for edit data source form ([#6742](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6742)) \ No newline at end of file diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx index be5a3a31be73..876e36bf56ff 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx @@ -12,6 +12,7 @@ import { existingDatasourceNamesList, mockDataSourceAttributesWithNoAuth, mockDataSourceAttributesWithRegisteredAuth, + mockDataSourceAttributesWithSigV4Auth, } from '../../../../mocks'; import { OpenSearchDashboardsContextProvider } from '../../../../../../opensearch_dashboards_react/public'; import { EditDataSourceForm } from './edit_data_source_form'; @@ -34,6 +35,13 @@ const usernameFieldIdentifier = 'datasourceUsername'; const usernameFormRowIdentifier = '[data-test-subj="editDatasourceUsernameFormRow"]'; const passwordFieldIdentifier = '[data-test-subj="updateDataSourceFormPasswordField"]'; const updatePasswordBtnIdentifier = '[data-test-subj="editDatasourceUpdatePasswordBtn"]'; +const updateAwsCredsBtnIdentifier = '[data-test-subj="editDatasourceUpdateAwsCredentialBtn"]'; +const regionFieldIdentifier = 'dataSourceRegion'; +const accessKeyFieldIdentifier = 'dataSourceAccessKey'; +const accessKeyFormRowIdentifier = '[data-test-subj="editDataSourceFormAccessKeyField"]'; +const secretKeyFieldIdentifier = 'dataSourceSecretKey'; +const secretKeyFormRowIdentifier = '[data-test-subj="editDataSourceFormSecretKeyField"]'; + describe('Datasource Management: Edit Datasource Form', () => { const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); mockedContext.authenticationMethodRegistry.registerAuthenticationMethod( @@ -351,6 +359,192 @@ describe('Datasource Management: Edit Datasource Form', () => { expect(mockFn).toHaveBeenCalled(); }); }); + + describe('Case 3: With AWSsigv4', () => { + beforeEach(() => { + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + component.update(); + }); + + test('should render normally', () => { + // @ts-ignore + expect(component.find({ name: titleFieldIdentifier }).first().props().value).toBe( + mockDataSourceAttributesWithSigV4Auth.title + ); + expect(component.find(endpointFieldIdentifier).first().props().disabled).toBe(true); + }); + + /* Validation */ + test('should validate title as required field & no duplicates allowed', () => { + /* Validate empty title - required */ + updateInputFieldAndBlur(component, titleFieldIdentifier, ''); + // @ts-ignore + expect(component.find(titleFormRowIdentifier).first().props().isInvalid).toBe(true); + + /* Validate duplicate title */ + updateInputFieldAndBlur(component, titleFieldIdentifier, 'DuP20'); + // @ts-ignore + expect(component.find(titleFormRowIdentifier).first().props().isInvalid).toBe(true); + + /* change to original title */ + updateInputFieldAndBlur( + component, + titleFieldIdentifier, + mockDataSourceAttributesWithSigV4Auth.title + ); + // @ts-ignore + expect(component.find(titleFormRowIdentifier).first().props().isInvalid).toBe(false); + + /* change to valid updated title */ + updateInputFieldAndBlur(component, titleFieldIdentifier, 'test007'); + // @ts-ignore + expect(component.find(titleFormRowIdentifier).first().props().isInvalid).toBe(false); + }); + test('should validate access key as required field', () => { + /* Validate empty accessKey - required */ + updateInputFieldAndBlur(component, accessKeyFieldIdentifier, ''); + // @ts-ignore + expect(component.find(accessKeyFormRowIdentifier).first().props().isInvalid).toBe(true); + + /* change to original accessKey */ + updateInputFieldAndBlur( + component, + accessKeyFieldIdentifier, + mockDataSourceAttributesWithSigV4Auth.auth.credentials.accessKey + ); + // @ts-ignore + expect(component.find(accessKeyFormRowIdentifier).first().props().isInvalid).toBe(false); + /* change to valid updated accessKey */ + updateInputFieldAndBlur(component, accessKeyFieldIdentifier, 'test123'); + // @ts-ignore + expect(component.find(accessKeyFormRowIdentifier).first().props().isInvalid).toBe(false); + }); + test('should validate secret key as required field', () => { + /* Validate empty secretKey - required */ + updateInputFieldAndBlur(component, secretKeyFieldIdentifier, ''); + // @ts-ignore + expect(component.find(secretKeyFormRowIdentifier).first().props().isInvalid).toBe(true); + + /* change to original secretKey */ + updateInputFieldAndBlur( + component, + secretKeyFieldIdentifier, + mockDataSourceAttributesWithSigV4Auth.auth.credentials.secretKey + ); + // @ts-ignore + expect(component.find(secretKeyFormRowIdentifier).first().props().isInvalid).toBe(false); + /* change to valid updated secretKey */ + updateInputFieldAndBlur(component, secretKeyFieldIdentifier, 'test123'); + // @ts-ignore + expect(component.find(secretKeyFormRowIdentifier).first().props().isInvalid).toBe(false); + }); + /* Functionality */ + test('should display update aws credential modal on update button click and should update the credentials', () => { + act(() => { + component.find(updateAwsCredsBtnIdentifier).first().simulate('click'); + }); + component.update(); + expect(component.find('UpdateAwsCredentialModal').exists()).toBe(true); + + /* Update password */ + act(() => { + // @ts-ignore + component.find('UpdateAwsCredentialModal').prop('handleUpdateAwsCredential')('test123'); + }); + component.update(); + expect(mockFn).toHaveBeenCalled(); + expect(component.find('UpdateAwsCredentialModal').exists()).toBe(false); + }); + test("should hide username & password fields when 'AWS Sigv4' is selected as the credential type", () => { + setAuthTypeValue(authTypeSelectIdentifier, AuthType.SigV4); + component.update(); + expect(component.find(usernameFormRowIdentifier).exists()).toBe(false); + expect(component.find(passwordFieldIdentifier).exists()).toBe(false); + }); + + /* Cancel Changes */ + test('should reset form on click cancel changes', async () => { + await new Promise((resolve) => + setTimeout(() => { + updateInputFieldAndBlur(component, descriptionFieldIdentifier, ''); + expect( + // @ts-ignore + component.find(descriptionFormRowIdentifier).first().props().isInvalid + ).toBeUndefined(); + resolve(); + }, 100) + ); + await new Promise((resolve) => + setTimeout(() => { + /* Updated description*/ + updateInputFieldAndBlur(component, descriptionFieldIdentifier, 'testDescription'); + expect( + // @ts-ignore + component.find(descriptionFormRowIdentifier).first().props().isInvalid + ).toBeUndefined(); + + expect(component.find('[data-test-subj="datasource-edit-cancelButton"]').exists()).toBe( + true + ); + component + .find('[data-test-subj="datasource-edit-cancelButton"]') + .first() + .simulate('click'); + resolve(); + }, 100) + ); + }); + + /* Save Changes */ + test('should update the form with Username&Password on click save changes', async () => { + await new Promise((resolve) => + setTimeout(() => { + updateInputFieldAndBlur(component, descriptionFieldIdentifier, ''); + expect( + // @ts-ignore + component.find(descriptionFormRowIdentifier).first().props().isInvalid + ).toBeUndefined(); + resolve(); + }, 100) + ); + await new Promise((resolve) => + setTimeout(() => { + /* Updated description*/ + updateInputFieldAndBlur(component, descriptionFieldIdentifier, 'testDescription'); + expect( + // @ts-ignore + component.find(descriptionFormRowIdentifier).first().props().isInvalid + ).toBeUndefined(); + + expect(component.find('[data-test-subj="datasource-edit-saveButton"]').exists()).toBe( + true + ); + component.find('[data-test-subj="datasource-edit-saveButton"]').first().simulate('click'); + expect(mockFn).toHaveBeenCalled(); + resolve(); + }, 100) + ); + }); + }); }); describe('With Registered Authentication', () => { diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index 63336cca5d32..e227e5e2087c 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -882,6 +882,7 @@ export class EditDataSourceForm extends React.Component diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/update_aws_credential_modal.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/update_aws_credential_modal.test.tsx new file mode 100644 index 000000000000..e7c7f0209438 --- /dev/null +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/update_aws_credential_modal.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { render } from '@testing-library/react'; +import { UpdateAwsCredentialModal } from './update_aws_credential_modal'; +import { SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; +import { EuiFormRow, EuiModalHeaderTitle } from '@elastic/eui'; +import { FormattedMessage } from 'react-intl'; + +describe('UpdateAwsCredentialModal', () => { + const mockHandleUpdateAwsCredential = jest.fn(); + const mockCloseUpdateAwsCredentialModal = jest.fn(); + + const props = { + region: 'us-east-1', + service: SigV4ServiceName.OpenSearch, + handleUpdateAwsCredential: mockHandleUpdateAwsCredential, + closeUpdateAwsCredentialModal: mockCloseUpdateAwsCredentialModal, + }; + + it('updates new access key state on input change', () => { + const wrapper = shallow(); + const newAccessKeyInput = wrapper.find('[name="updatedAccessKey"]'); + newAccessKeyInput.simulate('change', { target: { value: 'new_access_key' } }); + expect(wrapper.find('[name="updatedAccessKey"]').prop('value')).toEqual('new_access_key'); + }); + + it('renders modal with correct header title', () => { + const wrapper = shallow(); + const headerTitle = wrapper.find(EuiModalHeaderTitle).props().children; + expect(headerTitle).toEqual( +

+ +

+ ); + }); + + it('renders modal with correct label for updated secret key', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFormRow).at(4).props().label).toEqual('Updated secret key'); + }); + + it('renders modal with correct label for updated access key', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFormRow).at(3).props().label).toEqual('Updated access key'); + }); + + it('renders modal with correct region', () => { + const container = render(); + expect(container.getByTestId('data-source-update-credential-region')).toBeVisible(); + const text = container.getByTestId('data-source-update-credential-region'); + expect(text.textContent).toBe(props.region); + }); + + it('renders modal with service name select', () => { + const container = render(); + expect(container.getByTestId('data-source-update-credential-service-name')).toBeVisible(); + }); +}); diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index 3d991bd3e10a..0e5ec60bc307 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -267,6 +267,21 @@ export const mockDataSourceAttributesWithAuth = { }, }; +export const mockDataSourceAttributesWithSigV4Auth = { + id: 'test', + title: 'create-test-ds', + description: 'jest testing', + endpoint: 'https://test.com', + auth: { + type: AuthType.SigV4, + credentials: { + accessKey: 'test123', + secretKey: 'test123', + region: 'us-east-1', + }, + }, +}; + export const mockDataSourceAttributesWithNoAuth = { id: 'test123', title: 'create-test-ds123', From cae2a3faa443cd7afc63bf64e0f9c9dea03943da Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Thu, 9 May 2024 14:23:19 -0700 Subject: [PATCH 15/27] [MQL] support enhancing language selector (#6613) Enable with `data.enhancements.enabled: true` Allows for enhancing the data plugin UI service and search service. #### Remaining work * Address issue with time range being invalid if previous state successfully queried and set it with a time range format that is invalid for the new query language * For example, DQL with quick time range (4 weeks to now), get results. Switch to PPL, even though PPL has a default time range enhancement. The props date range saved in the app state takes priority and sets the time range to quick range causing an error. I can still modify the time range and get a successful query but it will first fail until the user updates it to a non quick time range. * Add tests * Disable for plugins that do not support the functionality * By default index patterns are created with a unique ID. However, it can be enabled to create an index pattern with a custom ID that matches the name of the index pattern (which in turn maps to indices). * For seamless integration, the temp data frame would need to check if the index pattern that maps to the data frame name. And get it's id. * This means that dashboards with visualizations that were created with an index pattern unique ID still require the existing index pattern to exist in memory. ### Issues Resolved closes #6639 closes #6311 partially resolves: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5504 * add error data frame Signed-off-by: Paul Sebastian move language to left, some styling and disable per app name Signed-off-by: Kawika Avilla --------- Signed-off-by: Kawika Avilla Signed-off-by: Paul Sebastian Co-authored-by: Paul Sebastian Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6613.yml | 2 + config/opensearch_dashboards.yml | 3 + package.json | 1 + packages/opensearch-datemath/index.d.ts | 2 + packages/opensearch-datemath/index.js | 4 +- packages/opensearch-datemath/index.test.ts | 44 +- .../src/cli_commands/snapshot.js | 7 + packages/osd-opensearch/src/cluster.js | 25 +- src/cli/serve/serve.js | 1 + .../dashboard_listing.test.tsx.snap | 140 ++++++ .../dashboard_top_nav.test.tsx.snap | 170 ++++++- src/plugins/data/common/constants.ts | 2 + .../data/common/data_frames/_df_cache.ts | 29 ++ .../data/common/data_frames/fields/index.ts | 6 + .../data/common/data_frames/fields/types.ts | 18 + src/plugins/data/common/data_frames/index.ts | 7 + src/plugins/data/common/data_frames/types.ts | 103 ++++ src/plugins/data/common/data_frames/utils.ts | 453 ++++++++++++++++++ src/plugins/data/common/index.ts | 1 + .../common/index_patterns/errors/index.ts | 1 + .../errors/missing_index_pattern.ts | 11 + .../common/index_patterns/fields/index.ts | 2 +- .../fields/index_pattern_field.ts | 5 +- .../common/index_patterns/fields/utils.ts | 15 +- .../index_patterns/_pattern_cache.ts | 4 + .../index_patterns/index_patterns.ts | 33 +- .../build_opensearch_query.ts | 11 + .../data/common/osd_field_types/index.ts | 2 + .../common/osd_field_types/osd_field_types.ts | 9 + .../common/search/opensearch_search/types.ts | 1 + .../create_search_source.test.ts | 5 + .../search_source/fetch/get_search_params.ts | 40 +- .../search/search_source/fetch/index.ts | 7 +- .../data/common/search/search_source/mocks.ts | 9 + .../search_source/search_source.test.ts | 5 + .../search/search_source/search_source.ts | 84 +++- .../data/common/search/search_source/types.ts | 3 +- src/plugins/data/common/search/types.ts | 5 +- src/plugins/data/common/types.ts | 19 + src/plugins/data/config.ts | 3 + .../public/data_sources/datasource/index.ts | 1 + .../datasource_service.test.ts | 22 +- .../data_sources/datasource_services/mocks.ts | 19 + .../register_default_datasource.ts | 2 + src/plugins/data/public/index.ts | 12 +- src/plugins/data/public/mocks.ts | 10 +- src/plugins/data/public/plugin.ts | 25 +- .../query_string/query_string_manager.ts | 6 +- src/plugins/data/public/search/mocks.ts | 7 + .../data/public/search/search_service.ts | 43 +- src/plugins/data/public/search/types.ts | 4 + src/plugins/data/public/services.ts | 2 + src/plugins/data/public/types.ts | 20 +- src/plugins/data/public/ui/index.ts | 1 + src/plugins/data/public/ui/mocks.ts | 46 ++ .../public/ui/query_string_input/_index.scss | 1 + .../_language_switcher.scss | 8 + .../language_switcher.test.tsx | 47 +- .../query_string_input/language_switcher.tsx | 182 ++++--- .../legacy_language_switcher.test.tsx | 55 +++ .../legacy_language_switcher.tsx | 118 +++++ .../query_bar_top_row.test.tsx | 2 + .../query_string_input/query_bar_top_row.tsx | 82 +++- .../query_string_input.test.tsx | 18 +- .../query_string_input/query_string_input.tsx | 72 ++- .../ui/search_bar/create_search_bar.tsx | 18 +- .../data/public/ui/search_bar/search_bar.tsx | 15 +- src/plugins/data/public/ui/settings/index.ts | 6 + src/plugins/data/public/ui/settings/mocks.ts | 18 + .../data/public/ui/settings/settings.ts | 93 ++++ src/plugins/data/public/ui/types.ts | 65 +++ src/plugins/data/public/ui/ui_service.ts | 70 +++ src/plugins/data/server/index.ts | 7 +- .../get_default_search_params.ts | 4 + .../opensearch_search_strategy.ts | 8 +- .../data/server/search/routes/call_msearch.ts | 8 +- .../data/server/search/search_service.ts | 47 +- src/plugins/data/server/search/types.ts | 9 +- src/plugins/data/server/ui_settings.ts | 57 +++ .../data_grid/data_grid_table_columns.tsx | 4 +- .../sidebar/discover_field_data_frame.tsx | 44 ++ .../sidebar/discover_sidebar.test.tsx | 2 + .../components/sidebar/discover_sidebar.tsx | 35 +- .../lib/display_index_pattern_creation.tsx | 17 + .../application/helpers/get_data_set.ts | 25 + .../view_components/panel/index.tsx | 31 +- .../utils/update_search_source.ts | 21 +- .../view_components/utils/use_search.ts | 20 +- src/plugins/discover/public/plugin.ts | 9 +- .../services/sample_data/data_sets/index.ts | 1 + .../sample_data/lib/sample_dataset_schema.ts | 6 + .../data_model/opensearch_query_parser.ts | 2 +- 92 files changed, 2484 insertions(+), 255 deletions(-) create mode 100644 changelogs/fragments/6613.yml create mode 100644 src/plugins/data/common/data_frames/_df_cache.ts create mode 100644 src/plugins/data/common/data_frames/fields/index.ts create mode 100644 src/plugins/data/common/data_frames/fields/types.ts create mode 100644 src/plugins/data/common/data_frames/index.ts create mode 100644 src/plugins/data/common/data_frames/types.ts create mode 100644 src/plugins/data/common/data_frames/utils.ts create mode 100644 src/plugins/data/common/index_patterns/errors/missing_index_pattern.ts create mode 100644 src/plugins/data/public/data_sources/datasource_services/mocks.ts create mode 100644 src/plugins/data/public/ui/mocks.ts create mode 100644 src/plugins/data/public/ui/query_string_input/_language_switcher.scss create mode 100644 src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx create mode 100644 src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx create mode 100644 src/plugins/data/public/ui/settings/index.ts create mode 100644 src/plugins/data/public/ui/settings/mocks.ts create mode 100644 src/plugins/data/public/ui/settings/settings.ts create mode 100644 src/plugins/data/public/ui/types.ts create mode 100644 src/plugins/data/public/ui/ui_service.ts create mode 100644 src/plugins/discover/public/application/components/sidebar/discover_field_data_frame.tsx create mode 100644 src/plugins/discover/public/application/components/sidebar/lib/display_index_pattern_creation.tsx create mode 100644 src/plugins/discover/public/application/helpers/get_data_set.ts diff --git a/changelogs/fragments/6613.yml b/changelogs/fragments/6613.yml new file mode 100644 index 000000000000..49cd01f52fdd --- /dev/null +++ b/changelogs/fragments/6613.yml @@ -0,0 +1,2 @@ +feat: +- Support language selector from the data plugin ([#6613](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6613)) \ No newline at end of file diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 0844db34c36c..8caf4ebf6af9 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -331,3 +331,6 @@ # Set the value to true to enable Ui Metric Collectors in Usage Collector # This publishes the Application Usage and UI Metrics into the saved object, which can be accessed by /api/stats?extended=true&legacy=true&exclude_usage=false # usageCollection.uiMetric.enabled: false + +# Set the value to true to enable enhancements for the data plugin +# data.enhancements.enabled: false diff --git a/package.json b/package.json index 2d9a7451af21..e55a565294dd 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "@hapi/vision": "^6.1.0", "@hapi/wreck": "^17.1.0", "@opensearch-project/opensearch": "^2.6.0", + "@opensearch/datemath": "5.0.3", "@osd/ace": "1.0.0", "@osd/analytics": "1.0.0", "@osd/apm-config-loader": "1.0.0", diff --git a/packages/opensearch-datemath/index.d.ts b/packages/opensearch-datemath/index.d.ts index d5a38f0176ea..0706d7d0dccf 100644 --- a/packages/opensearch-datemath/index.d.ts +++ b/packages/opensearch-datemath/index.d.ts @@ -43,6 +43,8 @@ declare const datemath: { unitsAsc: Unit[]; unitsDesc: Unit[]; + isDateTime(input: any): boolean; + /** * Parses a string into a moment object. The string can be something like "now - 15m". * @param options.forceNow If this optional parameter is supplied, "now" will be treated as this diff --git a/packages/opensearch-datemath/index.js b/packages/opensearch-datemath/index.js index 4367949d7cf0..ecbedf482922 100644 --- a/packages/opensearch-datemath/index.js +++ b/packages/opensearch-datemath/index.js @@ -49,6 +49,7 @@ const isDate = (d) => Object.prototype.toString.call(d) === '[object Date]'; const isValidDate = (d) => isDate(d) && !isNaN(d.valueOf()); +const isDateTime = (d, momentInstance = moment) => momentInstance.isMoment(d); /* * This is a simplified version of opensearch's date parser. * If you pass in a momentjs instance as the third parameter the calculation @@ -57,7 +58,7 @@ const isValidDate = (d) => isDate(d) && !isNaN(d.valueOf()); */ function parse(text, { roundUp = false, momentInstance = moment, forceNow } = {}) { if (!text) return undefined; - if (momentInstance.isMoment(text)) return text; + if (isDateTime(text, momentInstance)) return text; if (isDate(text)) return momentInstance(text); if (forceNow !== undefined && !isValidDate(forceNow)) { throw new Error('forceNow must be a valid Date'); @@ -164,6 +165,7 @@ function parseDateMath(mathString, time, roundUp) { module.exports = { parse: parse, + isDateTime: isDateTime, unitsMap: Object.freeze(unitsMap), units: Object.freeze(units), unitsAsc: Object.freeze(unitsAsc), diff --git a/packages/opensearch-datemath/index.test.ts b/packages/opensearch-datemath/index.test.ts index e293da72ac7f..fbd1973fa932 100644 --- a/packages/opensearch-datemath/index.test.ts +++ b/packages/opensearch-datemath/index.test.ts @@ -122,19 +122,19 @@ describe('dateMath', function () { }); it('should return a moment if passed a date', function () { - expect(dateMath.parse(date).format(format)).to.eql(mmnt.format(format)); + expect(dateMath.parse(date)!.format(format)).to.eql(mmnt.format(format)); }); it('should return a moment if passed an ISO8601 string', function () { - expect(dateMath.parse(string).format(format)).to.eql(mmnt.format(format)); + expect(dateMath.parse(string)!.format(format)).to.eql(mmnt.format(format)); }); it('should return the current time when parsing now', function () { - expect(dateMath.parse('now').format(format)).to.eql(now.format(format)); + expect(dateMath.parse('now')!.format(format)).to.eql(now.format(format)); }); it('should use the forceNow parameter when parsing now', function () { - expect(dateMath.parse('now', { forceNow: anchoredDate }).valueOf()).to.eql(unix); + expect(dateMath.parse('now', { forceNow: anchoredDate })!.valueOf()).to.eql(unix); }); }); @@ -158,17 +158,17 @@ describe('dateMath', function () { const thenEx = `${anchor}||-${len}${span}`; it('should return ' + len + span + ' ago', function () { - const parsed = dateMath.parse(nowEx).format(format); + const parsed = dateMath.parse(nowEx)!.format(format); expect(parsed).to.eql(now.subtract(len, span).format(format)); }); it('should return ' + len + span + ' before ' + anchor, function () { - const parsed = dateMath.parse(thenEx).format(format); + const parsed = dateMath.parse(thenEx)!.format(format); expect(parsed).to.eql(anchored.subtract(len, span).format(format)); }); it('should return ' + len + span + ' before forceNow', function () { - const parsed = dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf(); + const parsed = dateMath.parse(nowEx, { forceNow: anchoredDate })!.valueOf(); expect(parsed).to.eql(anchored.subtract(len, span).valueOf()); }); }); @@ -195,17 +195,17 @@ describe('dateMath', function () { const thenEx = `${anchor}||+${len}${span}`; it('should return ' + len + span + ' from now', function () { - expect(dateMath.parse(nowEx).format(format)).to.eql(now.add(len, span).format(format)); + expect(dateMath.parse(nowEx)!.format(format)).to.eql(now.add(len, span).format(format)); }); it('should return ' + len + span + ' after ' + anchor, function () { - expect(dateMath.parse(thenEx).format(format)).to.eql( + expect(dateMath.parse(thenEx)!.format(format)).to.eql( anchored.add(len, span).format(format) ); }); it('should return ' + len + span + ' after forceNow', function () { - expect(dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf()).to.eql( + expect(dateMath.parse(nowEx, { forceNow: anchoredDate })!.valueOf()).to.eql( anchored.add(len, span).valueOf() ); }); @@ -229,26 +229,26 @@ describe('dateMath', function () { spans.forEach((span) => { it(`should round now to the beginning of the ${span}`, function () { - expect(dateMath.parse('now/' + span).format(format)).to.eql( + expect(dateMath.parse('now/' + span)!.format(format)).to.eql( now.startOf(span).format(format) ); }); it(`should round now to the beginning of forceNow's ${span}`, function () { - expect(dateMath.parse('now/' + span, { forceNow: anchoredDate }).valueOf()).to.eql( + expect(dateMath.parse('now/' + span, { forceNow: anchoredDate })!.valueOf()).to.eql( anchored.startOf(span).valueOf() ); }); it(`should round now to the end of the ${span}`, function () { - expect(dateMath.parse('now/' + span, { roundUp: true }).format(format)).to.eql( + expect(dateMath.parse('now/' + span, { roundUp: true })!.format(format)).to.eql( now.endOf(span).format(format) ); }); it(`should round now to the end of forceNow's ${span}`, function () { expect( - dateMath.parse('now/' + span, { roundUp: true, forceNow: anchoredDate }).valueOf() + dateMath.parse('now/' + span, { roundUp: true, forceNow: anchoredDate })!.valueOf() ).to.eql(anchored.endOf(span).valueOf()); }); }); @@ -269,28 +269,28 @@ describe('dateMath', function () { }); it('should round to the nearest second with 0 value', function () { - const val = dateMath.parse('now-0s/s').format(format); + const val = dateMath.parse('now-0s/s')!.format(format); expect(val).to.eql(now.startOf('s').format(format)); }); it('should subtract 17s, rounded to the nearest second', function () { - const val = dateMath.parse('now-17s/s').format(format); + const val = dateMath.parse('now-17s/s')!.format(format); expect(val).to.eql(now.startOf('s').subtract(17, 's').format(format)); }); it('should add 555ms, rounded to the nearest millisecond', function () { - const val = dateMath.parse('now+555ms/ms').format(format); + const val = dateMath.parse('now+555ms/ms')!.format(format); expect(val).to.eql(now.add(555, 'ms').startOf('ms').format(format)); }); it('should subtract 555ms, rounded to the nearest second', function () { - const val = dateMath.parse('now-555ms/s').format(format); + const val = dateMath.parse('now-555ms/s')!.format(format); expect(val).to.eql(now.subtract(555, 'ms').startOf('s').format(format)); }); it('should round weeks to Sunday by default', function () { const val = dateMath.parse('now-1w/w'); - expect(val.isoWeekday()).to.eql(7); + expect(val!.isoWeekday()).to.eql(7); }); it('should round weeks based on the passed moment locale start of week setting', function () { @@ -300,7 +300,7 @@ describe('dateMath', function () { week: { dow: 2 }, }); const val = dateMath.parse('now-1w/w', { momentInstance: m }); - expect(val.isoWeekday()).to.eql(2); + expect(val!.isoWeekday()).to.eql(2); }); it('should round up weeks based on the passed moment locale start of week setting', function () { @@ -315,11 +315,11 @@ describe('dateMath', function () { }); // The end of the range (rounding up) should be the last day of the week (so one day before) // our start of the week, that's why 3 - 1 - expect(val.isoWeekday()).to.eql(3 - 1); + expect(val!.isoWeekday()).to.eql(3 - 1); }); it('should round relative to forceNow', function () { - const val = dateMath.parse('now-0s/s', { forceNow: anchoredDate }).valueOf(); + const val = dateMath.parse('now-0s/s', { forceNow: anchoredDate })!.valueOf(); expect(val).to.eql(anchored.startOf('s').valueOf()); }); diff --git a/packages/osd-opensearch/src/cli_commands/snapshot.js b/packages/osd-opensearch/src/cli_commands/snapshot.js index ff21dbe851c8..84d6acee104e 100644 --- a/packages/osd-opensearch/src/cli_commands/snapshot.js +++ b/packages/osd-opensearch/src/cli_commands/snapshot.js @@ -50,6 +50,7 @@ exports.help = (defaults = {}) => { --download-only Download the snapshot but don't actually start it --ssl Sets up SSL on OpenSearch --security Installs and sets up the OpenSearch Security plugin on the cluster + --sql Installs and sets up the required OpenSearch SQL/PPL plugins on the cluster --P OpenSearch plugin artifact URL to install it on the cluster. We can use the flag multiple times to install multiple plugins on the cluster snapshot. The argument value can be url to zip file, maven coordinates of the plugin or for local zip files, use file:. @@ -77,6 +78,8 @@ exports.run = async (defaults = {}) => { boolean: ['security'], + boolean: ['sql'], + default: defaults, }); @@ -98,6 +101,10 @@ exports.run = async (defaults = {}) => { await cluster.setupSecurity(installPath, options.version ?? defaults.version); } + if (options.sql) { + await cluster.setupSql(installPath, options.version ?? defaults.version); + } + options.bundledJDK = true; await cluster.run(installPath, options); diff --git a/packages/osd-opensearch/src/cluster.js b/packages/osd-opensearch/src/cluster.js index 455a1e5f919f..4b1c8b38259d 100644 --- a/packages/osd-opensearch/src/cluster.js +++ b/packages/osd-opensearch/src/cluster.js @@ -70,10 +70,11 @@ const first = (stream, map) => }); exports.Cluster = class Cluster { - constructor({ log = defaultLog, ssl = false, security = false } = {}) { + constructor({ log = defaultLog, ssl = false, security = false, sql = false } = {}) { this._log = log; this._ssl = ssl; this._security = security; + this._sql = sql; this._caCertPromise = ssl ? readFile(CA_CERT_PATH) : undefined; } @@ -224,6 +225,28 @@ exports.Cluster = class Cluster { } } + /** + * Setups cluster with SQL/PPL plugins + * + * @param {string} installPath + * @property {String} version - version of OpenSearch + */ + async setupSql(installPath, version) { + await this.installSqlPlugin(installPath, version, 'opensearch-sql'); + await this.installSqlPlugin(installPath, version, 'opensearch-observability'); + } + + async installSqlPlugin(installPath, version, id) { + this._log.info(`Setting up: ${id}`); + try { + const pluginUrl = generateEnginePluginUrl(version, id); + await this.installOpenSearchPlugins(installPath, pluginUrl); + this._log.info(`Completed setup: ${id}`); + } catch (ex) { + this._log.warning(`Failed to setup: ${id}`); + } + } + /** * Starts OpenSearch and returns resolved promise once started * diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index aed5d74a2c01..c1e489afb4ef 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -249,6 +249,7 @@ export default function (program) { .option('--dev', 'Run the server with development mode defaults') .option('--ssl', 'Run the dev server using HTTPS') .option('--security', 'Run the dev server using security defaults') + .option('--sql', 'Run the dev server using SQL/PPL defaults') .option('--dist', 'Use production assets from osd/optimizer') .option( '--no-base-path', diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index db34c4f229bb..4be28c0c4d08 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -276,6 +276,24 @@ exports[`dashboard listing hideWriteControls 1`] = ` "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -457,6 +475,7 @@ exports[`dashboard listing hideWriteControls 1`] = ` }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -465,6 +484,12 @@ exports[`dashboard listing hideWriteControls 1`] = ` "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -475,6 +500,9 @@ exports[`dashboard listing hideWriteControls 1`] = ` "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -1421,6 +1449,24 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -1602,6 +1648,7 @@ exports[`dashboard listing render table listing with initial filters from URL 1` }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -1610,6 +1657,12 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -1620,6 +1673,9 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -2627,6 +2683,24 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -2808,6 +2882,7 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -2816,6 +2891,12 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -2826,6 +2907,9 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -3833,6 +3917,24 @@ exports[`dashboard listing renders table rows 1`] = ` "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -4014,6 +4116,7 @@ exports[`dashboard listing renders table rows 1`] = ` }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -4022,6 +4125,12 @@ exports[`dashboard listing renders table rows 1`] = ` "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -4032,6 +4141,9 @@ exports[`dashboard listing renders table rows 1`] = ` "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -5039,6 +5151,24 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -5220,6 +5350,7 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -5228,6 +5359,12 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -5238,6 +5375,9 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 3649379ec583..5f71c7d5d217 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -260,6 +260,24 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -349,6 +367,7 @@ exports[`Dashboard top nav render in embed mode 1`] = ` }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -357,6 +376,12 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -367,6 +392,9 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -1231,6 +1259,24 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -1320,6 +1366,7 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -1328,6 +1375,12 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -1338,6 +1391,9 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -2202,6 +2258,24 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -2291,6 +2365,7 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -2299,6 +2374,12 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -2309,6 +2390,9 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -3173,6 +3257,24 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -3262,6 +3364,7 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -3270,6 +3373,12 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -3280,6 +3389,9 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -4144,6 +4256,24 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -4233,6 +4363,7 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -4241,6 +4372,12 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -4251,6 +4388,9 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -5115,6 +5255,24 @@ exports[`Dashboard top nav render with all components 1`] = ` "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [MockFunction], }, + "dataSources": Object { + "dataSourceFactory": DataSourceFactory { + "dataSourceClasses": Object {}, + }, + "dataSourceService": DataSourceService { + "dataSourceFetchers": Object {}, + "dataSources": Object {}, + "dataSourcesSubject": BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, + }, "fieldFormats": Object { "deserialize": [MockFunction], "getByFieldType": [MockFunction], @@ -5204,6 +5362,7 @@ exports[`Dashboard top nav render with all components 1`] = ` }, }, "search": Object { + "__enhance": [MockFunction], "aggs": Object { "calculateAutoTimeExpression": [Function], "createAggConfigs": [MockFunction], @@ -5212,6 +5371,12 @@ exports[`Dashboard top nav render with all components 1`] = ` "getAll": [Function], }, }, + "df": Object { + "clear": [MockFunction], + "get": [MockFunction], + "set": [MockFunction], + }, + "getDefaultSearchInterceptor": [MockFunction], "search": [MockFunction], "searchSource": Object { "create": [MockFunction], @@ -5222,6 +5387,9 @@ exports[`Dashboard top nav render with all components 1`] = ` "ui": Object { "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], + "Settings": undefined, + "isEnhancementsEnabled": false, + "queryEnhancements": Map {}, }, }, "docLinks": Object { @@ -5823,4 +5991,4 @@ exports[`Dashboard top nav render with all components 1`] = ` -`; \ No newline at end of file +`; diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 43db1fe72b9e..27cfc64cf2f6 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -35,6 +35,7 @@ export const UI_SETTINGS = { DOC_HIGHLIGHT: 'doc_table:highlight', QUERY_STRING_OPTIONS: 'query:queryString:options', QUERY_ALLOW_LEADING_WILDCARDS: 'query:allowLeadingWildcards', + QUERY_DATA_SOURCE_READONLY: 'query:dataSourceReadOnly', SEARCH_QUERY_LANGUAGE: 'search:queryLanguage', SORT_OPTIONS: 'sort:options', COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: 'courier:ignoreFilterIfFieldNotInIndex', @@ -60,4 +61,5 @@ export const UI_SETTINGS = { INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', + DATAFRAME_HYDRATION_STRATEGY: 'dataframe:hydrationStrategy', } as const; diff --git a/src/plugins/data/common/data_frames/_df_cache.ts b/src/plugins/data/common/data_frames/_df_cache.ts new file mode 100644 index 000000000000..177f26840874 --- /dev/null +++ b/src/plugins/data/common/data_frames/_df_cache.ts @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IDataFrame } from '..'; + +export interface DfCache { + get: () => IDataFrame | undefined; + set: (value: IDataFrame) => IDataFrame; + clear: () => void; +} + +export function createDataFrameCache(): DfCache { + let df: IDataFrame | undefined; + const cache: DfCache = { + get: () => { + return df; + }, + set: (prom: IDataFrame) => { + df = prom; + return prom; + }, + clear: () => { + df = undefined; + }, + }; + return cache; +} diff --git a/src/plugins/data/common/data_frames/fields/index.ts b/src/plugins/data/common/data_frames/fields/index.ts new file mode 100644 index 000000000000..9f269633f307 --- /dev/null +++ b/src/plugins/data/common/data_frames/fields/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; diff --git a/src/plugins/data/common/data_frames/fields/types.ts b/src/plugins/data/common/data_frames/fields/types.ts new file mode 100644 index 000000000000..47144c0c0198 --- /dev/null +++ b/src/plugins/data/common/data_frames/fields/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface IFieldType { + name: string; + type: string; + values: any[]; + count?: number; + aggregatable?: boolean; + filterable?: boolean; + searchable?: boolean; + sortable?: boolean; + visualizable?: boolean; + displayName?: string; + format?: any; +} diff --git a/src/plugins/data/common/data_frames/index.ts b/src/plugins/data/common/data_frames/index.ts new file mode 100644 index 000000000000..8b6a31eaea68 --- /dev/null +++ b/src/plugins/data/common/data_frames/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; +export * from './utils'; diff --git a/src/plugins/data/common/data_frames/types.ts b/src/plugins/data/common/data_frames/types.ts new file mode 100644 index 000000000000..f8dd04193cff --- /dev/null +++ b/src/plugins/data/common/data_frames/types.ts @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SearchResponse } from 'elasticsearch'; +import { IFieldType } from './fields'; + +export * from './_df_cache'; + +/** @public **/ +export enum DATA_FRAME_TYPES { + DEFAULT = 'data_frame', + POLLING = 'data_frame_polling', +} + +export interface DataFrameService { + get: () => IDataFrame | undefined; + set: (dataFrame: IDataFrame) => Promise; + clear: () => void; +} + +/** + * A data frame is a two-dimensional labeled data structure with columns of potentially different types. + */ +export interface IDataFrame { + type?: DATA_FRAME_TYPES.DEFAULT; + name?: string; + schema?: Array>; + meta?: Record; + fields: IFieldType[]; + size: number; +} + +/** + * An aggregation is a process where the values of multiple rows are grouped together to form a single summary value. + */ +export interface DataFrameAgg { + value: number; +} + +/** + * A bucket aggregation is a type of aggregation that creates buckets or sets of data. + */ +export interface DataFrameBucketAgg extends DataFrameAgg { + key: string; +} + +/** + * This configuration is used to define how the aggregation should be performed. + */ +export interface DataFrameAggConfig { + id: string; + type: string; + field?: string; + order?: Record; + size?: number; + date_histogram?: { + field: string; + fixed_interval?: string; + calendar_interval?: string; + time_zone: string; + min_doc_count: number; + }; + avg?: { + field: string; + }; + cardinality?: { + field: string; + }; + terms?: { + field: string; + size: number; + order: Record; + }; + aggs?: Record; +} + +export interface PartialDataFrame extends Omit { + fields: Array>; +} + +/** + * To be utilize with aggregations and will map to buckets + * Plugins can get the aggregated value by their own logic + * Setting to null will disable the aggregation if plugin wishes + * In future, if the plugin doesn't intentionally set the value to null, + * we can calculate the value based on the fields. + */ +// TODO: handle composite +export interface IDataFrameWithAggs extends IDataFrame { + aggs: Record; +} + +export interface IDataFrameResponse extends SearchResponse { + type: DATA_FRAME_TYPES; + body: IDataFrame | IDataFrameWithAggs | IDataFrameError; + took: number; +} + +export interface IDataFrameError { + error: Error; +} diff --git a/src/plugins/data/common/data_frames/utils.ts b/src/plugins/data/common/data_frames/utils.ts new file mode 100644 index 000000000000..c3c55c5f227c --- /dev/null +++ b/src/plugins/data/common/data_frames/utils.ts @@ -0,0 +1,453 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SearchResponse } from 'elasticsearch'; +import datemath from '@opensearch/datemath'; +import { + DATA_FRAME_TYPES, + DataFrameAggConfig, + DataFrameBucketAgg, + IDataFrame, + IDataFrameWithAggs, + IDataFrameResponse, + PartialDataFrame, +} from './types'; +import { IFieldType } from './fields'; +import { IndexPatternFieldMap, IndexPatternSpec } from '../index_patterns'; +import { IOpenSearchDashboardsSearchRequest } from '../search'; +import { GetAggTypeFn, GetDataFrameAggQsFn } from '../types'; + +/** + * Returns the raw data frame from the search request. + * + * @param searchRequest - search request object. + * @returns dataframe + */ +export const getRawDataFrame = (searchRequest: IOpenSearchDashboardsSearchRequest) => { + return searchRequest.params?.body?.df; +}; + +/** + * Returns the raw query string from the search request. + * Gets current state query if exists, otherwise gets the initial query. + * + * @param searchRequest - search request object + * @returns query string + */ +export const getRawQueryString = ( + searchRequest: IOpenSearchDashboardsSearchRequest +): string | undefined => { + return ( + searchRequest.params?.body?.query?.queries[1]?.query ?? + searchRequest.params?.body?.query?.queries[0]?.query + ); +}; + +/** + * Returns the raw aggregations from the search request. + * + * @param searchRequest - search request object + * @returns aggregations + */ +export const getRawAggs = (searchRequest: IOpenSearchDashboardsSearchRequest) => { + return searchRequest.params?.body?.aggs; +}; + +/** + * Returns the unique values for raw aggregations. This is used + * with `other-filter` aggregation. To get the values that were not + * included in the aggregation response prior to this request. + * + * @param rawAggs - raw aggregations object + * @returns object containing the field and its unique values + */ +export const getUniqueValuesForRawAggs = (rawAggs: Record) => { + const filters = rawAggs.filters?.filters?.['']?.bool?.must_not; + if (!filters || !Array.isArray(filters)) { + return null; + } + const values: unknown[] = []; + let field: string | undefined; + + filters.forEach((agg: any) => { + Object.values(agg).forEach((aggValue) => { + Object.entries(aggValue as Record).forEach(([key, value]) => { + field = key; + values.push(value); + }); + }); + }); + + return { field, values }; +}; + +/** + * Returns the aggregation configuration for raw aggregations. + * Aggregations are nested objects, so this function recursively + * builds an object that is easier to work with. + * + * @param rawAggs - raw aggregations object + * @returns aggregation configuration + */ +export const getAggConfigForRawAggs = (rawAggs: Record): DataFrameAggConfig | null => { + const aggConfig: DataFrameAggConfig = { id: '', type: '' }; + + Object.entries(rawAggs).forEach(([aggKey, agg]) => { + aggConfig.id = aggKey; + Object.entries(agg as Record).forEach(([name, value]) => { + if (name === 'aggs') { + aggConfig.aggs = {}; + Object.entries(value as Record).forEach(([subAggKey, subRawAgg]) => { + const subAgg = getAggConfigForRawAggs(subRawAgg as Record); + if (subAgg) { + aggConfig.aggs![subAgg.id] = { ...subAgg, id: subAggKey }; + } + }); + } else { + aggConfig.type = name; + Object.assign(aggConfig, { [name]: value }); + } + }); + }); + + return aggConfig; +}; + +/** + * Returns the aggregation configuration. + * + * @param searchRequest - search request object + * @param aggConfig - aggregation configuration object + * @param getAggTypeFn - function to get the aggregation type from the aggsService + * @returns aggregation configuration + */ +export const getAggConfig = ( + searchRequest: IOpenSearchDashboardsSearchRequest, + aggConfig: Partial = {}, + getAggTypeFn: GetAggTypeFn +): DataFrameAggConfig => { + const rawAggs = getRawAggs(searchRequest); + Object.entries(rawAggs).forEach(([aggKey, agg]) => { + aggConfig.id = aggKey; + Object.entries(agg as Record).forEach(([name, value]) => { + if (name === 'aggs' && value) { + aggConfig.aggs = {}; + Object.entries(value as Record).forEach(([subAggKey, subRawAgg]) => { + const subAgg = getAggConfigForRawAggs(subRawAgg as Record); + if (subAgg) { + aggConfig.aggs![subAgg.id] = { ...subAgg, id: subAggKey }; + } + }); + } else { + aggConfig.type = getAggTypeFn(name)?.type ?? name; + Object.assign(aggConfig, { [name]: value }); + } + }); + }); + + return aggConfig as DataFrameAggConfig; +}; + +/** + * Converts the data frame response to a search response. + * This function is used to convert the data frame response to a search response + * to be used by the rest of the application. + * + * @param response - data frame response object + * @returns converted search response + */ +export const convertResult = (response: IDataFrameResponse): SearchResponse => { + const body = response.body; + if (body.hasOwnProperty('error')) { + return response; + } + const data = body as IDataFrame; + const hits: any[] = []; + for (let index = 0; index < data.size; index++) { + const hit: { [key: string]: any } = {}; + data.fields.forEach((field) => { + hit[field.name] = field.values[index]; + }); + hits.push({ + _index: data.name, + _source: hit, + }); + } + const searchResponse: SearchResponse = { + took: response.took, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 0, + max_score: 0, + hits, + }, + }; + + if (data.hasOwnProperty('aggs')) { + const dataWithAggs = data as IDataFrameWithAggs; + if (!dataWithAggs.aggs) { + // TODO: MQL best guess, get timestamp field and caculate it here + return searchResponse; + } + searchResponse.aggregations = Object.entries(dataWithAggs.aggs).reduce( + (acc: Record, [id, value]) => { + const aggConfig = dataWithAggs.meta?.aggs; + if (id === 'other-filter') { + const buckets = value as DataFrameBucketAgg[]; + buckets.forEach((bucket) => { + const bucketValue = bucket.value; + searchResponse.hits.total += bucketValue; + }); + acc[id] = { + buckets: [{ '': { doc_count: 0 } }], + }; + return acc; + } + if (aggConfig && aggConfig.type === 'buckets') { + const buckets = value as DataFrameBucketAgg[]; + acc[id] = { + buckets: buckets.map((bucket) => { + const bucketValue = bucket.value; + searchResponse.hits.total += bucketValue; + return { + key_as_string: bucket.key, + key: (aggConfig as DataFrameAggConfig).date_histogram + ? new Date(bucket.key).getTime() + : bucket.key, + doc_count: bucketValue, + }; + }), + }; + return acc; + } + acc[id] = Array.isArray(value) ? value[0] : value; + return acc; + }, + {} + ); + } + + return searchResponse; +}; + +/** + * Formats the field value. + * + * @param field - field object + * @param value - value to format + * @returns formatted value + */ +export const formatFieldValue = (field: IFieldType | Partial, value: any): any => { + return field.format && field.format.convert ? field.format.convert(value) : value; +}; + +/** + * Returns the field type. This function is used to determine the field type so that can + * be used by the rest of the application. The field type must map to a OsdFieldType + * to be used by the rest of the application. + * + * @param field - field object + * @returns field type + */ +export const getFieldType = (field: IFieldType | Partial): string | undefined => { + const fieldName = field.name?.toLowerCase(); + if (fieldName?.includes('date') || fieldName?.includes('timestamp')) { + return 'date'; + } + if (field.values?.some((value) => value instanceof Date || datemath.isDateTime(value))) { + return 'date'; + } + if (field.type === 'struct') { + return 'object'; + } + + return field.type; +}; + +/** + * Returns the time field. If there is an aggConfig then we do not have to guess. + * If there is no aggConfig then we will try to guess the time field. + * + * @param data - data frame object. + * @param aggConfig - aggregation configuration object. + * @returns time field. + */ +export const getTimeField = ( + data: IDataFrame, + aggConfig?: DataFrameAggConfig +): Partial | undefined => { + const fields = data.schema || data.fields; + return aggConfig && aggConfig.date_histogram && aggConfig.date_histogram.field + ? fields.find((field) => field.name === aggConfig?.date_histogram?.field) + : fields.find((field) => field.type === 'date'); +}; + +/** + * Checks if the value is a GeoPoint. Expects an object with lat and lon properties. + * + * @param value - value to check + * @returns True if the value is a GeoPoint, false otherwise + */ +export const isGeoPoint = (value: any): boolean => { + return ( + typeof value === 'object' && + value !== null && + 'lat' in value && + 'lon' in value && + typeof value.lat === 'number' && + typeof value.lon === 'number' + ); +}; + +/** + * Creates a data frame. + * + * @param partial - partial data frame object + * @returns data frame. + */ +export const createDataFrame = (partial: PartialDataFrame): IDataFrame | IDataFrameWithAggs => { + let size = 0; + const processField = (field: any) => { + field.type = getFieldType(field); + if (!field.values) { + field.values = new Array(size); + } else if (field.values.length > size) { + size = field.values.length; + } + return field as IFieldType; + }; + + const schema = partial.schema?.map(processField); + const fields = partial.fields?.map(processField); + + return { + ...partial, + schema, + fields, + size, + }; +}; + +/** + * Updates the data frame metadata. Metadata is used to store the aggregation configuration. + * It also stores the query string used to fetch the data frame aggregations. + * + * @param params - { dataFrame, qs, aggConfig, timeField, timeFilter, getAggQsFn } + */ +export const updateDataFrameMeta = ({ + dataFrame, + qs, + aggConfig, + timeField, + timeFilter, + getAggQsFn, +}: { + dataFrame: IDataFrame; + qs: string; + aggConfig: DataFrameAggConfig; + timeField: any; + timeFilter: string; + getAggQsFn: GetDataFrameAggQsFn; +}) => { + dataFrame.meta = { + aggs: aggConfig, + aggsQs: { + [aggConfig.id]: getAggQsFn({ + qs, + aggConfig, + timeField, + timeFilter, + }), + }, + }; + + if (aggConfig.aggs) { + const subAggs = aggConfig.aggs as Record; + for (const [key, subAgg] of Object.entries(subAggs)) { + const subAggConfig: Record = { [key]: subAgg }; + dataFrame.meta.aggsQs[subAgg.id] = getAggQsFn({ + qs, + aggConfig: subAggConfig as DataFrameAggConfig, + timeField, + timeFilter, + }); + } + } +}; + +/** + * Converts a data frame to index pattern spec which can be used to create an index pattern. + * + * @param dataFrame - data frame object + * @param id - index pattern id if it exists + * @returns index pattern spec + */ +export const dataFrameToSpec = (dataFrame: IDataFrame, id?: string): IndexPatternSpec => { + const fields = (dataFrame.schema || dataFrame.fields) as IFieldType[]; + + const toFieldSpec = (field: IFieldType, overrides: Partial) => { + return { + ...field, + ...overrides, + aggregatable: field.aggregatable ?? true, + searchable: field.searchable ?? true, + }; + }; + + const flattenFields = (acc: IndexPatternFieldMap, field: IFieldType): any => { + switch (field.type) { + case 'object': + const dataField = dataFrame.fields.find((f) => f.name === field.name) || field; + if (dataField) { + const subField = dataField.values[0]; + if (!subField) { + acc[field.name] = toFieldSpec(field, {}); + break; + } + Object.entries(subField).forEach(([key, value]) => { + const subFieldName = `${dataField.name}.${key}`; + const subFieldType = typeof value; + if (subFieldType === 'object' && isGeoPoint(value)) { + acc[subFieldName] = toFieldSpec(subField, { + name: subFieldName, + type: 'geo_point', + }); + } else { + acc = flattenFields(acc, { + name: subFieldName, + type: subFieldType, + values: + subFieldType === 'object' + ? Object.entries(value as Record)?.map(([k, v]) => ({ + name: `${subFieldName}.${k}`, + type: typeof v, + })) + : [], + } as IFieldType); + } + }); + } + break; + default: + acc[field.name] = toFieldSpec(field, {}); + break; + } + return acc; + }; + + return { + id: id ?? DATA_FRAME_TYPES.DEFAULT, + title: dataFrame.name, + timeFieldName: getTimeField(dataFrame)?.name, + type: !id ? DATA_FRAME_TYPES.DEFAULT : undefined, + fields: fields.reduce(flattenFields, {} as IndexPatternFieldMap), + }; +}; diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 1eefb2383f8b..d7b7e56e2280 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -30,6 +30,7 @@ export * from './constants'; export * from './opensearch_query'; +export * from './data_frames'; export * from './field_formats'; export * from './field_mapping'; export * from './index_patterns'; diff --git a/src/plugins/data/common/index_patterns/errors/index.ts b/src/plugins/data/common/index_patterns/errors/index.ts index 9d2a1175dd3f..d77b7f140afe 100644 --- a/src/plugins/data/common/index_patterns/errors/index.ts +++ b/src/plugins/data/common/index_patterns/errors/index.ts @@ -29,3 +29,4 @@ */ export * from './duplicate_index_pattern'; +export * from './missing_index_pattern'; diff --git a/src/plugins/data/common/index_patterns/errors/missing_index_pattern.ts b/src/plugins/data/common/index_patterns/errors/missing_index_pattern.ts new file mode 100644 index 000000000000..7d5fa3356314 --- /dev/null +++ b/src/plugins/data/common/index_patterns/errors/missing_index_pattern.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export class MissingIndexPatternError extends Error { + constructor(message: string) { + super(message); + this.name = 'MissingIndexPatternError'; + } +} diff --git a/src/plugins/data/common/index_patterns/fields/index.ts b/src/plugins/data/common/index_patterns/fields/index.ts index 351c0d3b7593..58a8612fd8c9 100644 --- a/src/plugins/data/common/index_patterns/fields/index.ts +++ b/src/plugins/data/common/index_patterns/fields/index.ts @@ -29,6 +29,6 @@ */ export * from './types'; -export { isFilterable, isNestedField } from './utils'; +export { isFilterable, isNestedField, setOverrides, getOverrides } from './utils'; export * from './field_list'; export * from './index_pattern_field'; diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index e1882abb9722..ce9dd5a67eba 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -28,7 +28,7 @@ * under the License. */ -import { OsdFieldType, getOsdFieldType } from '../../osd_field_types'; +import { OsdFieldType, getOsdFieldOverrides, getOsdFieldType } from '../../osd_field_types'; import { OSD_FIELD_TYPES } from '../../osd_field_types/types'; import { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; @@ -133,6 +133,7 @@ export class IndexPatternField implements IFieldType { } public get filterable() { + if (getOsdFieldOverrides().filterable !== undefined) return !!getOsdFieldOverrides().filterable; return ( this.name === '_id' || this.scripted || @@ -141,6 +142,8 @@ export class IndexPatternField implements IFieldType { } public get visualizable() { + if (getOsdFieldOverrides().visualizable !== undefined) + return !!getOsdFieldOverrides().visualizable; const notVisualizableFieldTypes: string[] = [OSD_FIELD_TYPES.UNKNOWN, OSD_FIELD_TYPES.CONFLICT]; return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type); } diff --git a/src/plugins/data/common/index_patterns/fields/utils.ts b/src/plugins/data/common/index_patterns/fields/utils.ts index 5178568533d2..1b96cd3b7063 100644 --- a/src/plugins/data/common/index_patterns/fields/utils.ts +++ b/src/plugins/data/common/index_patterns/fields/utils.ts @@ -28,12 +28,25 @@ * under the License. */ -import { getFilterableOsdTypeNames } from '../../osd_field_types'; +import { + getFilterableOsdTypeNames, + getOsdFieldOverrides, + setOsdFieldOverrides, +} from '../../osd_field_types'; import { IFieldType } from './types'; const filterableTypes = getFilterableOsdTypeNames(); +export function setOverrides(overrides: Record | undefined) { + setOsdFieldOverrides(overrides); +} + +export function getOverrides(): Record { + return getOsdFieldOverrides(); +} + export function isFilterable(field: IFieldType): boolean { + if (getOverrides().filterable !== undefined) return !!getOverrides().filterable; return ( field.name === '_id' || field.scripted || diff --git a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts index cc54e7b4cdde..834d21e6c35d 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts @@ -32,6 +32,7 @@ import { IndexPattern } from './index_pattern'; export interface PatternCache { get: (id: string) => IndexPattern; + getByTitle: (title: string) => IndexPattern; set: (id: string, value: IndexPattern) => IndexPattern; clear: (id: string) => void; clearAll: () => void; @@ -43,6 +44,9 @@ export function createIndexPatternCache(): PatternCache { get: (id: string) => { return vals[id]; }, + getByTitle: (title: string) => { + return Object.values(vals).find((pattern: IndexPattern) => pattern.title === title); + }, set: (id: string, prom: any) => { vals[id] = prom; return prom; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 688605821097..d60c1b6dd901 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -55,7 +55,7 @@ import { UI_SETTINGS, SavedObject } from '../../../common'; import { SavedObjectNotFound } from '../../../../opensearch_dashboards_utils/common'; import { IndexPatternMissingIndices } from '../lib'; import { findByTitle, getIndexPatternTitle } from '../utils'; -import { DuplicateIndexPatternError } from '../errors'; +import { DuplicateIndexPatternError, MissingIndexPatternError } from '../errors'; const indexPatternCache = createIndexPatternCache(); const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; @@ -192,8 +192,10 @@ export class IndexPatternsService { * Clear index pattern list cache * @param id optionally clear a single id */ - clearCache = (id?: string) => { - this.savedObjectsCache = null; + clearCache = (id?: string, clearSavedObjectsCache: boolean = true) => { + if (clearSavedObjectsCache) { + this.savedObjectsCache = null; + } if (id) { indexPatternCache.clear(id); } else { @@ -208,6 +210,10 @@ export class IndexPatternsService { return this.savedObjectsCache; }; + saveToCache = (id: string, indexPattern: IndexPattern) => { + indexPatternCache.set(id, indexPattern); + }; + /** * Get default index pattern */ @@ -282,9 +288,13 @@ export class IndexPatternsService { * Refresh field list for a given index pattern * @param indexPattern */ - refreshFields = async (indexPattern: IndexPattern) => { + refreshFields = async (indexPattern: IndexPattern, skipType = false) => { try { - const fields = await this.getFieldsForIndexPattern(indexPattern); + const indexPatternCopy = skipType + ? ({ ...indexPattern, type: undefined } as IndexPattern) + : indexPattern; + + const fields = await this.getFieldsForIndexPattern(indexPatternCopy); const scripted = indexPattern.getScriptedFields().map((field) => field.spec); indexPattern.fields.replaceAll([...fields, ...scripted]); } catch (err) { @@ -499,6 +509,19 @@ export class IndexPatternsService { return indexPattern; }; + /** + * Get an index pattern by title if cached + * @param id + */ + + getByTitle = (title: string, ignoreErrors: boolean = false): IndexPattern => { + const indexPattern = indexPatternCache.getByTitle(title); + if (!indexPattern && !ignoreErrors) { + throw new MissingIndexPatternError(`Missing index pattern: ${title}`); + } + return indexPattern; + }; + migrate(indexPattern: IndexPattern, newTitle: string) { return this.savedObjectsClient .update( diff --git a/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts b/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts index 481eae12d121..23295c3bafec 100644 --- a/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts +++ b/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts @@ -66,6 +66,17 @@ export function buildOpenSearchQuery( const validQueries = queries.filter((query) => has(query, 'query')); const queriesByLanguage = groupBy(validQueries, 'language'); + const unsupportedQueries = Object.keys(queriesByLanguage).filter( + (language) => language !== 'kuery' && language.toLowerCase() !== 'lucene' + ); + if (unsupportedQueries.length > 0) { + return { + type: 'unsupported', + queries, + filters, + }; + } + const kueryQuery = buildQueryFromKuery( indexPattern, queriesByLanguage.kuery, diff --git a/src/plugins/data/common/osd_field_types/index.ts b/src/plugins/data/common/osd_field_types/index.ts index 9454adc2d475..345a22094334 100644 --- a/src/plugins/data/common/osd_field_types/index.ts +++ b/src/plugins/data/common/osd_field_types/index.ts @@ -35,4 +35,6 @@ export { getOsdFieldType, getOsdTypeNames, getFilterableOsdTypeNames, + getOsdFieldOverrides, + setOsdFieldOverrides, } from './osd_field_types'; diff --git a/src/plugins/data/common/osd_field_types/osd_field_types.ts b/src/plugins/data/common/osd_field_types/osd_field_types.ts index b9fe14ff1a87..00fb67a8dc5e 100644 --- a/src/plugins/data/common/osd_field_types/osd_field_types.ts +++ b/src/plugins/data/common/osd_field_types/osd_field_types.ts @@ -34,6 +34,7 @@ import { OPENSEARCH_FIELD_TYPES, OSD_FIELD_TYPES } from './types'; /** @private */ const registeredOsdTypes = createOsdFieldTypes(); +let osdFieldOverrides = {}; /** * Get a type object by name @@ -75,3 +76,11 @@ export const castOpenSearchToOsdFieldTypeName = ( */ export const getFilterableOsdTypeNames = (): string[] => registeredOsdTypes.filter((type) => type.filterable).map((type) => type.name); + +export const setOsdFieldOverrides = (newOverrides: { [key: string]: any } | undefined) => { + osdFieldOverrides = newOverrides ? Object.assign({}, osdFieldOverrides, newOverrides) : {}; +}; + +export const getOsdFieldOverrides = (): { [key: string]: any } => { + return osdFieldOverrides; +}; diff --git a/src/plugins/data/common/search/opensearch_search/types.ts b/src/plugins/data/common/search/opensearch_search/types.ts index 3b93177bf201..f90a3f1de245 100644 --- a/src/plugins/data/common/search/opensearch_search/types.ts +++ b/src/plugins/data/common/search/opensearch_search/types.ts @@ -57,6 +57,7 @@ export type ISearchRequestParams> = { export interface IOpenSearchSearchRequest extends IOpenSearchDashboardsSearchRequest { indexType?: string; + language?: string; dataSourceId?: string; } diff --git a/src/plugins/data/common/search/search_source/create_search_source.test.ts b/src/plugins/data/common/search/search_source/create_search_source.test.ts index 467ecec59f5a..68dfa7699e13 100644 --- a/src/plugins/data/common/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/common/search/search_source/create_search_source.test.ts @@ -50,6 +50,11 @@ describe('createSearchSource', () => { callMsearch: jest.fn(), loadingCount$: new BehaviorSubject(0), }, + df: { + get: jest.fn().mockReturnValue({}), + set: jest.fn().mockReturnValue({}), + clear: jest.fn(), + }, }; indexPatternContractMock = ({ diff --git a/src/plugins/data/common/search/search_source/fetch/get_search_params.ts b/src/plugins/data/common/search/search_source/fetch/get_search_params.ts index a25d6e530bad..d9bd7721d6cb 100644 --- a/src/plugins/data/common/search/search_source/fetch/get_search_params.ts +++ b/src/plugins/data/common/search/search_source/fetch/get_search_params.ts @@ -29,7 +29,7 @@ */ import { UI_SETTINGS } from '../../../constants'; -import { GetConfigFn } from '../../../types'; +import { GetConfigFn, GetDataFrameFn, DestroyDataFrameFn } from '../../../types'; import { ISearchRequestParams } from '../../index'; import { SearchRequest } from './types'; @@ -49,16 +49,50 @@ export function getPreference(getConfig: GetConfigFn) { : undefined; } +export function getExternalSearchParamsFromRequest( + searchRequest: SearchRequest, + dependencies: { + getConfig: GetConfigFn; + getDataFrame: GetDataFrameFn; + } +): ISearchRequestParams { + const { getConfig, getDataFrame } = dependencies; + const searchParams = getSearchParams(getConfig); + const dataFrame = getDataFrame(); + const indexTitle = searchRequest.index.title || searchRequest.index; + + return { + index: indexTitle, + body: { + ...searchRequest.body, + ...(dataFrame && dataFrame?.name === indexTitle ? { df: dataFrame } : {}), + }, + ...searchParams, + }; +} + /** @public */ // TODO: Could provide this on runtime contract with dependencies // already wired up. export function getSearchParamsFromRequest( searchRequest: SearchRequest, - dependencies: { getConfig: GetConfigFn } + dependencies: { + getConfig: GetConfigFn; + getDataFrame?: GetDataFrameFn; + destroyDataFrame?: DestroyDataFrameFn; + } ): ISearchRequestParams { - const { getConfig } = dependencies; + const { getConfig, getDataFrame, destroyDataFrame } = dependencies; const searchParams = getSearchParams(getConfig); + if (getDataFrame && destroyDataFrame) { + if (getDataFrame()) { + delete searchRequest.body.df; + delete searchRequest.indexType; + destroyDataFrame(); + } + } + return { index: searchRequest.index.title || searchRequest.index, body: searchRequest.body, diff --git a/src/plugins/data/common/search/search_source/fetch/index.ts b/src/plugins/data/common/search/search_source/fetch/index.ts index bb432ec0d833..f171ca38738e 100644 --- a/src/plugins/data/common/search/search_source/fetch/index.ts +++ b/src/plugins/data/common/search/search_source/fetch/index.ts @@ -28,6 +28,11 @@ * under the License. */ -export { getSearchParams, getSearchParamsFromRequest, getPreference } from './get_search_params'; +export { + getSearchParams, + getExternalSearchParamsFromRequest, + getSearchParamsFromRequest, + getPreference, +} from './get_search_params'; export { RequestFailure } from './request_error'; export * from './types'; diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index 959d1aebfe53..83ea582ef99f 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -47,6 +47,8 @@ export const searchSourceInstanceMock: MockedKeys = { createChild: jest.fn().mockReturnThis(), setParent: jest.fn(), getParent: jest.fn().mockReturnThis(), + setDataFrame: jest.fn(), + getDataFrame: jest.fn().mockReturnThis(), fetch: jest.fn().mockResolvedValue({}), onRequestStart: jest.fn(), getSearchRequestBody: jest.fn(), @@ -54,6 +56,8 @@ export const searchSourceInstanceMock: MockedKeys = { history: [], getSerializedFields: jest.fn(), serialize: jest.fn(), + flatten: jest.fn().mockReturnThis(), + destroyDataFrame: jest.fn(), }; export const searchSourceCommonMock: jest.Mocked = { @@ -70,4 +74,9 @@ export const createSearchSourceMock = (fields?: SearchSourceFields) => callMsearch: jest.fn(), loadingCount$: new BehaviorSubject(0), }, + df: { + get: jest.fn().mockReturnValue({}), + set: jest.fn().mockReturnValue({}), + clear: jest.fn(), + }, }); diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 92cc0682a136..09adc867d213 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -84,6 +84,11 @@ describe('SearchSource', () => { callMsearch: jest.fn(), loadingCount$: new BehaviorSubject(0), }, + df: { + get: jest.fn().mockReturnValue({}), + set: jest.fn().mockReturnValue({}), + clear: jest.fn(), + }, }; }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index abe6fa1b5cb4..b7dc48b404b6 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -87,10 +87,17 @@ import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../opensearch_dashboards_utils/common'; import { IIndexPattern } from '../../index_patterns'; +import { DATA_FRAME_TYPES, IDataFrame, IDataFrameResponse, convertResult } from '../../data_frames'; import { IOpenSearchSearchRequest, IOpenSearchSearchResponse, ISearchOptions } from '../..'; import { IOpenSearchDashboardsSearchRequest, IOpenSearchDashboardsSearchResponse } from '../types'; import { ISearchSource, SearchSourceOptions, SearchSourceFields } from './types'; -import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch'; +import { + FetchHandlers, + RequestFailure, + getExternalSearchParamsFromRequest, + getSearchParamsFromRequest, + SearchRequest, +} from './fetch'; import { getOpenSearchQueryConfig, @@ -116,6 +123,7 @@ export const searchSourceRequiredUiSettings = [ UI_SETTINGS.QUERY_STRING_OPTIONS, UI_SETTINGS.SEARCH_INCLUDE_FROZEN, UI_SETTINGS.SORT_OPTIONS, + UI_SETTINGS.DATAFRAME_HYDRATION_STRATEGY, ]; export interface SearchSourceDependencies extends FetchHandlers { @@ -123,11 +131,18 @@ export interface SearchSourceDependencies extends FetchHandlers { // search options required here and returning a promise instead of observable. search: < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( request: SearchStrategyRequest, options: ISearchOptions ) => Promise; + df: { + get: () => IDataFrame | undefined; + set: (dataFrame: IDataFrame) => Promise; + clear: () => void; + }; } /** @public **/ @@ -267,6 +282,36 @@ export class SearchSource { return this.parent; } + /** + * Get the data frame of this SearchSource + * @return {undefined|IDataFrame} + */ + getDataFrame() { + return this.dependencies.df.get(); + } + + /** + * Set the data frame of this SearchSource + * + * @async + * @return {undefined|IDataFrame} + */ + async setDataFrame(dataFrame: IDataFrame | undefined) { + if (dataFrame) { + await this.dependencies.df.set(dataFrame); + } else { + this.destroyDataFrame(); + } + return this.getDataFrame(); + } + + /** + * Clear the data frame of this SearchSource + */ + destroyDataFrame() { + this.dependencies.df.clear(); + } + /** * Fetch this source and reject the returned Promise on error * @@ -282,6 +327,8 @@ export class SearchSource { let response; if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { response = await this.legacyFetch(searchRequest, options); + } else if (this.isUnsupportedRequest(searchRequest)) { + response = await this.fetchExternalSearch(searchRequest, options); } else { const indexPattern = this.getField('index'); searchRequest.dataSourceId = indexPattern?.dataSourceRef?.id; @@ -337,12 +384,39 @@ export class SearchSource { const params = getSearchParamsFromRequest(searchRequest, { getConfig, + getDataFrame: this.getDataFrame.bind(this), + destroyDataFrame: this.destroyDataFrame.bind(this), }); return search( { params, indexType: searchRequest.indexType, dataSourceId: searchRequest.dataSourceId }, options - ).then(({ rawResponse }) => onResponse(searchRequest, rawResponse)); + ).then((response: any) => onResponse(searchRequest, response.rawResponse)); + } + + /** + * Run a non-native search using the search service + * @return {Promise>} + */ + private async fetchExternalSearch(searchRequest: SearchRequest, options: ISearchOptions) { + const { search, getConfig, onResponse } = this.dependencies; + + const params = getExternalSearchParamsFromRequest(searchRequest, { + getConfig, + getDataFrame: this.getDataFrame.bind(this), + }); + + return search({ params }, options).then(async (response: any) => { + if (response.hasOwnProperty('type')) { + if ((response as IDataFrameResponse).type === DATA_FRAME_TYPES.DEFAULT) { + const dataFrameResponse = response as IDataFrameResponse; + await this.setDataFrame(dataFrameResponse.body as IDataFrame); + return onResponse(searchRequest, convertResult(response as IDataFrameResponse)); + } + // TODO: MQL else if data_frame_polling then poll for the data frame updating the df fields only + } + return onResponse(searchRequest, response.rawResponse); + }); } /** @@ -366,6 +440,10 @@ export class SearchSource { ); } + private isUnsupportedRequest(request: SearchRequest): boolean { + return request.body!.query.hasOwnProperty('type') && request.body!.query.type === 'unsupported'; + } + /** * Called by requests of this search source when they are started * @param options diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 13b100aab1a7..9f3fd75e1ce9 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -29,7 +29,7 @@ */ import { NameList } from 'elasticsearch'; -import { Filter, IndexPattern, Query } from '../..'; +import { Filter, IDataFrame, IndexPattern, Query } from '../..'; import { SearchSource } from './search_source'; /** @@ -103,6 +103,7 @@ export interface SearchSourceFields { searchAfter?: OpenSearchQuerySearchAfter; timeout?: string; terminate_after?: number; + df?: IDataFrame; } export interface SearchSourceOptions { diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index e05c0adb46f0..d2bcdc0f4d05 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -34,6 +34,7 @@ import { IOpenSearchSearchResponse, ISearchOptions, } from '../../common/search'; +import { IDataFrameResponse } from '../data_frames'; export type ISearch = ( request: IOpenSearchDashboardsSearchRequest, @@ -42,7 +43,9 @@ export type ISearch = ( export type ISearchGeneric = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( request: SearchStrategyRequest, options?: ISearchOptions diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index 361ba39edfec..6a1f6e5a99d3 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -28,9 +28,13 @@ * under the License. */ +import { DataFrameAggConfig, IDataFrame } from './data_frames'; +import { BucketAggType, MetricAggType } from './search'; + export * from './query/types'; export * from './osd_field_types/types'; export * from './index_patterns/types'; +export * from './data_frames/types'; /** * If a service is being shared on both the client and the server, and @@ -43,3 +47,18 @@ export * from './index_patterns/types'; * not possible. */ export type GetConfigFn = (key: string, defaultOverride?: T) => T; +export type GetDataFrameFn = () => IDataFrame | undefined; +export type GetDataFrameAggQsFn = ({ + qs, + aggConfig, + timeField, + timeFilter, +}: { + qs: string; + aggConfig: DataFrameAggConfig; + timeField: any; + timeFilter: any; +}) => any; + +export type DestroyDataFrameFn = () => void; +export type GetAggTypeFn = (id: string) => BucketAggType | MetricAggType; diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index f8b553f3da1b..f8dcad85fb49 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -31,6 +31,9 @@ import { schema, TypeOf } from '@osd/config-schema'; export const configSchema = schema.object({ + enhancements: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), autocomplete: schema.object({ querySuggestions: schema.object({ enabled: schema.boolean({ defaultValue: true }), diff --git a/src/plugins/data/public/data_sources/datasource/index.ts b/src/plugins/data/public/data_sources/datasource/index.ts index e45cd6dad22c..7cc4f8e6549a 100644 --- a/src/plugins/data/public/data_sources/datasource/index.ts +++ b/src/plugins/data/public/data_sources/datasource/index.ts @@ -12,5 +12,6 @@ export { IDataSourceQueryResult, DataSourceConnectionStatus, IndexPatternOption, + IDataSourceDataSet, } from './types'; export { DataSourceFactory } from './factory'; diff --git a/src/plugins/data/public/data_sources/datasource_services/datasource_service.test.ts b/src/plugins/data/public/data_sources/datasource_services/datasource_service.test.ts index e1d2077f60e5..1182ffe65d8a 100644 --- a/src/plugins/data/public/data_sources/datasource_services/datasource_service.test.ts +++ b/src/plugins/data/public/data_sources/datasource_services/datasource_service.test.ts @@ -5,7 +5,11 @@ import { waitFor } from '@testing-library/dom'; import { DataSource } from '../datasource'; -import { IndexPatternsService } from '../../index_patterns'; +import { SavedObject } from '../../../../../core/public'; +import { + IndexPatternSavedObjectAttrs, + IndexPatternsService, +} from '../../index_patterns/index_patterns'; import { DataSourceService } from '../datasource_services'; import { LocalDSDataSetParams, @@ -55,8 +59,20 @@ class MockDataSource extends DataSource< } async getDataSet(dataSetParams?: LocalDSDataSetParams): Promise { - await this.indexPattern.ensureDefaultIndexPattern(); - return await this.indexPattern.getCache(); + const savedObjectLst = await this.indexPattern.getCache(); + + if (!Array.isArray(savedObjectLst)) { + return { dataSets: [] }; + } + + return { + dataSets: savedObjectLst.map((savedObject: SavedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + }; + }), + }; } async testConnection(): Promise { diff --git a/src/plugins/data/public/data_sources/datasource_services/mocks.ts b/src/plugins/data/public/data_sources/datasource_services/mocks.ts new file mode 100644 index 000000000000..1bf4d4d7a9a4 --- /dev/null +++ b/src/plugins/data/public/data_sources/datasource_services/mocks.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataSourceService } from './datasource_service'; +import { DataSourceStart } from './types'; +import { DataSourceFactory } from '../datasource'; + +function createStartContract(): jest.Mocked { + return { + dataSourceService: DataSourceService.getInstance(), + dataSourceFactory: DataSourceFactory.getInstance(), + }; +} + +export const dataSourceServiceMock = { + createStartContract, +}; diff --git a/src/plugins/data/public/data_sources/register_default_datasource.ts b/src/plugins/data/public/data_sources/register_default_datasource.ts index ce169955704b..d8ae19114c3c 100644 --- a/src/plugins/data/public/data_sources/register_default_datasource.ts +++ b/src/plugins/data/public/data_sources/register_default_datasource.ts @@ -39,3 +39,5 @@ export const registerDefaultDataSource = (data: Omit>; export type Start = jest.Mocked>; @@ -59,7 +61,7 @@ const createSetupContract = (): Setup => { }; }; -const createStartContract = (): Start => { +const createStartContract = (isEnhancementsEnabled: boolean = false): Start => { const queryStartMock = queryServiceMock.createStartContract(); return { actions: { @@ -70,10 +72,7 @@ const createStartContract = (): Start => { search: searchServiceMock.createStartContract(), fieldFormats: fieldFormatsServiceMock.createStartContract(), query: queryStartMock, - ui: { - IndexPatternSelect: jest.fn(), - SearchBar: jest.fn().mockReturnValue(null), - }, + ui: uiServiceMock.createStartContract(isEnhancementsEnabled), indexPatterns: ({ find: jest.fn((search) => [{ id: search, title: search }]), createField: jest.fn(() => {}), @@ -93,6 +92,7 @@ const createStartContract = (): Start => { ), clearCache: jest.fn(), } as unknown) as IndexPatternsContract, + dataSources: dataSourceServiceMock.createStartContract(), }; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 4917eb9db9e2..f22a61423d1d 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -46,9 +46,9 @@ import { } from './types'; import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; +import { UiService } from './ui/ui_service'; import { FieldFormatsService } from './field_formats'; import { QueryService } from './query'; -import { createIndexPatternSelect } from './ui/index_pattern_select'; import { IndexPatternsService, onRedirectNoIndexPattern, @@ -63,9 +63,9 @@ import { setOverlays, setQueryService, setSearchService, + setUiService, setUiSettings, } from './services'; -import { createSearchBar } from './ui/search_bar/create_search_bar'; import { opensearchaggs } from './search/expressions'; import { SELECT_RANGE_TRIGGER, @@ -112,12 +112,14 @@ export class DataPublicPlugin > { private readonly autocomplete: AutocompleteService; private readonly searchService: SearchService; + private readonly uiService: UiService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; private readonly storage: IStorageWrapper; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); + this.uiService = new UiService(initializerContext); this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); this.autocomplete = new AutocompleteService(initializerContext); @@ -161,13 +163,17 @@ export class DataPublicPlugin expressions, }); + const uiService = this.uiService.setup(core, {}); + return { + // TODO: MQL autocomplete: this.autocomplete.setup(core), search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService, __enhance: (enhancements: DataPublicPluginEnhancements) => { - searchService.__enhance(enhancements.search); + if (enhancements.search) searchService.__enhance(enhancements.search); + if (enhancements.ui) uiService.__enhance(enhancements.ui); }, }; } @@ -246,18 +252,12 @@ export class DataPublicPlugin registerDefaultDataSource(dataServices); - const SearchBar = createSearchBar({ - core, - data: dataServices, - storage: this.storage, - }); + const uiService = this.uiService.start(core, { dataServices, storage: this.storage }); + setUiService(uiService); return { ...dataServices, - ui: { - IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), - SearchBar, - }, + ui: uiService, }; } @@ -265,5 +265,6 @@ export class DataPublicPlugin this.autocomplete.clearProviders(); this.queryService.stop(); this.searchService.stop(); + this.uiService.stop(); } } diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index bee5d4c3ded9..3747cabaf9ca 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -44,6 +44,10 @@ export class QueryStringManager { this.query$ = new BehaviorSubject(this.getDefaultQuery()); } + private getDefaultQueryString() { + return this.storage.get('opensearchDashboards.userQueryString') || ''; + } + private getDefaultLanguage() { return ( this.storage.get('opensearchDashboards.userQueryLanguage') || @@ -53,7 +57,7 @@ export class QueryStringManager { public getDefaultQuery() { return { - query: '', + query: this.getDefaultQueryString(), language: this.getDefaultLanguage(), }; } diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 32c0ef61aca6..736de838df8d 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -45,6 +45,13 @@ function createStartContract(): jest.Mocked { search: jest.fn(), showError: jest.fn(), searchSource: searchSourceMock.createStartContract(), + __enhance: jest.fn(), + getDefaultSearchInterceptor: jest.fn(), + df: { + get: jest.fn().mockReturnValue({}), + set: jest.fn().mockReturnValue({}), + clear: jest.fn(), + }, }; } diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index c73e7881faa6..340c007963af 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -57,6 +57,13 @@ import { getShardDelayBucketAgg, } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; +import { + DataFrameService, + IDataFrame, + IDataFrameResponse, + createDataFrameCache, + dataFrameToSpec, +} from '../../common/data_frames'; /** @internal */ export interface SearchServiceSetupDependencies { @@ -73,7 +80,9 @@ export interface SearchServiceStartDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); private readonly searchSourceService = new SearchSourceService(); + private readonly dfCache = createDataFrameCache(); private searchInterceptor!: ISearchInterceptor; + private defaultSearchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; constructor(private initializerContext: PluginInitializerContext) {} @@ -95,6 +104,7 @@ export class SearchService implements Plugin { startServices: getStartServices(), usageCollector: this.usageCollector!, }); + this.defaultSearchInterceptor = this.searchInterceptor; expressions.registerFunction(opensearchdsl); expressions.registerType(opensearchRawResponse); @@ -129,11 +139,36 @@ export class SearchService implements Plugin { const loadingCount$ = new BehaviorSubject(0); http.addLoadingCountSource(loadingCount$); + const dfService: DataFrameService = { + get: () => this.dfCache.get(), + set: async (dataFrame: IDataFrame) => { + if (this.dfCache.get() && this.dfCache.get()?.name !== dataFrame.name) { + indexPatterns.clearCache(this.dfCache.get()!.name, false); + } + this.dfCache.set(dataFrame); + const existingIndexPattern = indexPatterns.getByTitle(dataFrame.name!, true); + const dataSet = await indexPatterns.create( + dataFrameToSpec(dataFrame, existingIndexPattern?.id), + !existingIndexPattern?.id + ); + // save to cache by title because the id is not unique for temporary index pattern created + indexPatterns.saveToCache(dataSet.title, dataSet); + }, + clear: () => { + if (this.dfCache.get() === undefined) return; + // name because the id is not unique for temporary index pattern created + indexPatterns.clearCache(this.dfCache.get()!.name, false); + this.dfCache.clear(); + }, + }; + const searchSourceDependencies: SearchSourceDependencies = { getConfig: uiSettings.get.bind(uiSettings), search: < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( request: SearchStrategyRequest, options: ISearchOptions @@ -145,6 +180,7 @@ export class SearchService implements Plugin { callMsearch: getCallMsearch({ http }), loadingCount$, }, + df: dfService, }; return { @@ -154,6 +190,11 @@ export class SearchService implements Plugin { this.searchInterceptor.showError(e); }, searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), + __enhance: (enhancements: SearchEnhancements) => { + this.searchInterceptor = enhancements.searchInterceptor; + }, + getDefaultSearchInterceptor: () => this.defaultSearchInterceptor, + df: dfService, }; } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 8a0d82b855c8..29dc37b41c91 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -35,6 +35,7 @@ import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } fr import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { DataFrameService } from '../../common/data_frames'; export { ISearchStartSearchSource }; @@ -78,6 +79,9 @@ export interface ISearchStart { * {@link ISearchStartSearchSource} */ searchSource: ISearchStartSearchSource; + __enhance: (enhancements: SearchEnhancements) => void; + getDefaultSearchInterceptor: () => ISearchInterceptor; + df: DataFrameService; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 3bcc9d69a9a4..d75dab2986ca 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -59,3 +59,5 @@ export const [getQueryService, setQueryService] = createGetterSetter< export const [getSearchService, setSearchService] = createGetterSetter< DataPublicPluginStart['search'] >('Search'); + +export const [getUiService, setUiService] = createGetterSetter('Ui'); diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 5870ea7def8e..4f7936006a94 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -28,7 +28,6 @@ * under the License. */ -import React from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; @@ -39,12 +38,13 @@ import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternsContract } from './index_patterns'; -import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { DataSourceStart } from './data_sources/datasource_services/types'; +import { IUiStart, UiEnhancements } from './ui'; export interface DataPublicPluginEnhancements { - search: SearchEnhancements; + search?: SearchEnhancements; + ui?: UiEnhancements; } export interface DataSetupDependencies { @@ -71,14 +71,6 @@ export interface DataPublicPluginSetup { __enhance: (enhancements: DataPublicPluginEnhancements) => void; } -/** - * Data plugin prewired UI components - */ -export interface DataPublicPluginStartUi { - IndexPatternSelect: React.ComponentType; - SearchBar: React.ComponentType; -} - /** * utilities to generate filters from action context */ @@ -122,10 +114,10 @@ export interface DataPublicPluginStart { */ query: QueryStart; /** - * prewired UI components - * {@link DataPublicPluginStartUi} + * UI components service + * {@link IUiStart} */ - ui: DataPublicPluginStartUi; + ui: IUiStart; /** * multiple datasources * {@link DataSourceStart} diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index c618df1783b5..98e6d393ce63 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -28,6 +28,7 @@ * under the License. */ +export { UiEnhancements, IUiStart, createSettings, Settings, DataSettings } from './types'; export { IndexPatternSelectProps } from './index_pattern_select'; export { FilterLabel } from './filter_bar'; export { QueryStringInput, QueryStringInputProps } from './query_string_input'; diff --git a/src/plugins/data/public/ui/mocks.ts b/src/plugins/data/public/ui/mocks.ts new file mode 100644 index 000000000000..47d3f059f504 --- /dev/null +++ b/src/plugins/data/public/ui/mocks.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SettingsMock } from './settings/mocks'; +import { IUiSetup, IUiStart } from './types'; + +const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, +}); + +const createMockStorage = () => ({ + storage: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), +}); + +function createSetupContract(): jest.Mocked { + return { + __enhance: jest.fn(), + }; +} + +function createStartContract(isEnhancementsEnabled: boolean = false): jest.Mocked { + const queryEnhancements = new Map(); + return { + isEnhancementsEnabled, + queryEnhancements, + IndexPatternSelect: jest.fn(), + SearchBar: jest.fn(), + Settings: new SettingsMock(createMockStorage(), queryEnhancements), + }; +} + +export const uiServiceMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/data/public/ui/query_string_input/_index.scss b/src/plugins/data/public/ui/query_string_input/_index.scss index 8686490016c5..f21b9cbb4327 100644 --- a/src/plugins/data/public/ui/query_string_input/_index.scss +++ b/src/plugins/data/public/ui/query_string_input/_index.scss @@ -1 +1,2 @@ @import "./query_bar"; +@import "./language_switcher" diff --git a/src/plugins/data/public/ui/query_string_input/_language_switcher.scss b/src/plugins/data/public/ui/query_string_input/_language_switcher.scss new file mode 100644 index 000000000000..176d072c102b --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/_language_switcher.scss @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.languageSelect { + max-width: 150px; + transform: translateY(-1px) translateX(-0.5px); +} diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx index 22ec4e9edd96..dd1a3b4674cd 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx @@ -33,9 +33,28 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; import { coreMock } from '../../../../../core/public/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; +import { EuiComboBox } from '@elastic/eui'; +import { QueryEnhancement } from '../types'; + const startMock = coreMock.createStart(); +jest.mock('../../services', () => ({ + getUiService: () => ({ + isEnhancementsEnabled: true, + queryEnhancements: new Map(), + Settings: { + setUiOverridesByUserQueryLanguage: jest.fn(), + }, + }), + getSearchService: () => ({ + __enhance: jest.fn(), + df: { + clear: jest.fn(), + }, + getDefaultSearchInterceptor: jest.fn(), + }), +})); + describe('LanguageSwitcher', () => { function wrapInContext(testProps: any) { const services = { @@ -50,7 +69,7 @@ describe('LanguageSwitcher', () => { ); } - it('should toggle off if language is lucene', () => { + it('should select lucene if language is lucene', () => { const component = mountWithIntl( wrapInContext({ language: 'lucene', @@ -59,12 +78,17 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); - expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); + const euiComboBox = component.find(EuiComboBox); + expect(euiComboBox.prop('selectedOptions')).toEqual( + expect.arrayContaining([ + { + label: 'Lucene', + }, + ]) + ); }); - it('should toggle on if language is kuery', () => { + it('should select DQL if language is kuery', () => { const component = mountWithIntl( wrapInContext({ language: 'kuery', @@ -73,8 +97,13 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); - expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); + const euiComboBox = component.find(EuiComboBox); + expect(euiComboBox.prop('selectedOptions')).toEqual( + expect.arrayContaining([ + { + label: 'DQL', + }, + ]) + ); }); }); diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index 816c21bc0848..a2d1f9ca41ce 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -28,116 +28,100 @@ * under the License. */ -import { - EuiButtonEmpty, - EuiForm, - EuiFormRow, - EuiLink, - EuiPopover, - EuiPopoverTitle, - EuiSpacer, - EuiSwitch, - EuiText, - PopoverAnchorPosition, -} from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; -import React, { useState } from 'react'; -import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { EuiComboBox, EuiComboBoxOptionOption, PopoverAnchorPosition } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React from 'react'; +import { getSearchService, getUiService } from '../../services'; interface Props { language: string; onSelectLanguage: (newLanguage: string) => void; anchorPosition?: PopoverAnchorPosition; + appName?: string; +} + +function mapExternalLanguageToOptions(language: string) { + return { + label: language, + value: language, + }; } export function QueryLanguageSwitcher(props: Props) { - const osdDQLDocs = useOpenSearchDashboards().services.docLinks?.links.opensearchDashboards.dql - .base; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const luceneLabel = ( - - ); - const dqlLabel = ( - - ); - const dqlFullName = ( - - ); + const dqlLabel = i18n.translate('data.query.queryBar.dqlLanguageName', { + defaultMessage: 'DQL', + }); + const luceneLabel = i18n.translate('data.query.queryBar.luceneLanguageName', { + defaultMessage: 'Lucene', + }); - const button = ( - setIsPopoverOpen(!isPopoverOpen)} - className="euiFormControlLayout__append dqlQueryBar__languageSwitcherButton" - data-test-subj={'switchQueryLanguageButton'} - > - {props.language === 'lucene' ? luceneLabel : dqlLabel} - - ); + const languageOptions: EuiComboBoxOptionOption[] = [ + { + label: dqlLabel, + value: 'kuery', + }, + { + label: luceneLabel, + value: 'lucene', + }, + ]; - return ( - setIsPopoverOpen(false)} - repositionOnScroll - > - - - -
- -

- - {dqlFullName} - - ), - }} - /> -

-
+ const uiService = getUiService(); + const searchService = getSearchService(); - + const queryEnhancements = uiService.queryEnhancements; + if (uiService.isEnhancementsEnabled) { + queryEnhancements.forEach((enhancement) => { + if ( + enhancement.supportedAppNames && + props.appName && + !enhancement.supportedAppNames.includes(props.appName) + ) + return; + languageOptions.push(mapExternalLanguageToOptions(enhancement.language)); + }); + } - - - - ) : ( - - ) - } - checked={props.language === 'kuery'} - onChange={() => { - const newLanguage = props.language === 'lucene' ? 'kuery' : 'lucene'; - props.onSelectLanguage(newLanguage); - }} - data-test-subj="languageToggle" - /> - - -
-
+ const selectedLanguage = { + label: + (languageOptions.find( + (option) => (option.value as string).toLowerCase() === props.language.toLowerCase() + )?.label as string) ?? languageOptions[0].label, + }; + + const setSearchEnhance = (queryLanguage: string) => { + if (!uiService.isEnhancementsEnabled) return; + const queryEnhancement = queryEnhancements.get(queryLanguage); + searchService.__enhance({ + searchInterceptor: queryEnhancement + ? queryEnhancement.search + : searchService.getDefaultSearchInterceptor(), + }); + + if (!queryEnhancement) { + searchService.df.clear(); + } + uiService.Settings.setUiOverridesByUserQueryLanguage(queryLanguage); + }; + + const handleLanguageChange = (newLanguage: EuiComboBoxOptionOption[]) => { + const queryLanguage = newLanguage[0].value as string; + props.onSelectLanguage(queryLanguage); + setSearchEnhance(queryLanguage); + }; + + setSearchEnhance(props.language); + + return ( + ); } diff --git a/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx b/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx new file mode 100644 index 000000000000..7e7e43190bb4 --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { LegacyQueryLanguageSwitcher } from './legacy_language_switcher'; +import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; +const startMock = coreMock.createStart(); + +describe('LegacyLanguageSwitcher', () => { + function wrapInContext(testProps: any) { + const services = { + uiSettings: startMock.uiSettings, + docLinks: startMock.docLinks, + }; + + return ( + + + + ); + } + + it('should toggle off if language is lucene', () => { + const component = mountWithIntl( + wrapInContext({ + language: 'lucene', + onSelectLanguage: () => { + return; + }, + }) + ); + component.find(EuiButtonEmpty).simulate('click'); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); + }); + + it('should toggle on if language is kuery', () => { + const component = mountWithIntl( + wrapInContext({ + language: 'kuery', + onSelectLanguage: () => { + return; + }, + }) + ); + component.find(EuiButtonEmpty).simulate('click'); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); + }); +}); diff --git a/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx new file mode 100644 index 000000000000..b8128a950d33 --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiLink, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + EuiSwitch, + EuiText, + PopoverAnchorPosition, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import React, { useState } from 'react'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; + +interface Props { + language: string; + onSelectLanguage: (newLanguage: string) => void; + anchorPosition?: PopoverAnchorPosition; +} + +export function LegacyQueryLanguageSwitcher(props: Props) { + const osdDQLDocs = useOpenSearchDashboards().services.docLinks?.links.opensearchDashboards.dql + .base; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const luceneLabel = ( + + ); + const dqlLabel = ( + + ); + const dqlFullName = ( + + ); + + const button = ( + setIsPopoverOpen(!isPopoverOpen)} + className="euiFormControlLayout__append dqlQueryBar__languageSwitcherButton" + data-test-subj={'switchQueryLanguageButton'} + > + {props.language === 'lucene' ? luceneLabel : dqlLabel} + + ); + + return ( + setIsPopoverOpen(false)} + repositionOnScroll + > + + + +
+ +

+ + {dqlFullName} + + ), + }} + /> +

+
+ + + + + + + ) : ( + + ) + } + checked={props.language === 'kuery'} + onChange={() => { + const newLanguage = props.language === 'lucene' ? 'kuery' : 'lucene'; + props.onSelectLanguage(newLanguage); + }} + data-test-subj="languageToggle" + /> + + +
+
+ ); +} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 3b8c41eb1a39..fa194054930a 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -70,6 +70,8 @@ startMock.uiSettings.get.mockImplementation((key: string) => { from: 'now-15m', to: 'now', }; + case UI_SETTINGS.QUERY_DATA_SOURCE_READONLY: + return false; default: throw new Error(`Unexpected config key: ${key}`); } diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 8c509d573e1b..027c90a6c798 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -46,6 +46,7 @@ import { import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { Toast } from 'src/core/public'; +import { isEqual, compact } from 'lodash'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { useOpenSearchDashboards, @@ -54,14 +55,18 @@ import { } from '../../../../opensearch_dashboards_react/public'; import QueryStringInputUI from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; -import { PersistedLog, getQueryLog } from '../../query'; +import { PersistedLog, fromUser, getQueryLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; +import { QueryEnhancement, Settings } from '../types'; const QueryStringInput = withOpenSearchDashboards(QueryStringInputUI); // @internal export interface QueryBarTopRowProps { query?: Query; + isEnhancementsEnabled?: boolean; + queryEnhancements?: Map; + settings?: Settings; onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; @@ -95,8 +100,22 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { const { uiSettings, notifications, storage, appName, docLinks } = opensearchDashboards.services; const osdDQLDocs: string = docLinks!.links.opensearchDashboards.dql.base; + const isDataSourceReadOnly = uiSettings.get('query:dataSourceReadOnly'); const queryLanguage = props.query && props.query.language; + const queryUiEnhancement = + (queryLanguage && + props.queryEnhancements && + props.queryEnhancements.get(queryLanguage)?.searchBar) || + null; + const parsedQuery = + !queryUiEnhancement || isValidQuery(props.query) + ? props.query! + : { query: getQueryStringInitialValue(queryLanguage!), language: queryLanguage! }; + if (!isEqual(parsedQuery?.query, props.query?.query)) { + onQueryChange(parsedQuery); + onSubmit({ query: parsedQuery, dateRange: getDateRange() }); + } const persistedLog: PersistedLog | undefined = React.useMemo( () => queryLanguage && uiSettings && storage && appName @@ -116,15 +135,19 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { function getDateRange() { const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); return { - from: props.dateRangeFrom || defaultTimeSetting.from, - to: props.dateRangeTo || defaultTimeSetting.to, + from: + props.dateRangeFrom || + queryUiEnhancement?.dateRange?.initialFrom || + defaultTimeSetting.from, + to: props.dateRangeTo || queryUiEnhancement?.dateRange?.initialTo || defaultTimeSetting.to, }; } - function onQueryChange(query: Query) { + function onQueryChange(query: Query, dateRange?: TimeRange) { + if (queryUiEnhancement && !isValidQuery(query)) return; props.onChange({ query, - dateRange: getDateRange(), + dateRange: dateRange ?? getDateRange(), }); } @@ -181,10 +204,10 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { props.onSubmit({ query, dateRange }); } - function onInputSubmit(query: Query) { + function onInputSubmit(query: Query, dateRange?: TimeRange) { onSubmit({ query, - dateRange: getDateRange(), + dateRange: dateRange ?? getDateRange(), }); } @@ -196,6 +219,38 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { return valueAsMoment.toISOString(); } + function isValidQuery(query: Query | undefined) { + if (!query || !query.query) return false; + return ( + !Array.isArray(props.indexPatterns!) || + compact(props.indexPatterns!).length === 0 || + !isDataSourceReadOnly || + fromUser(query!.query).includes( + typeof props.indexPatterns[0] === 'string' + ? props.indexPatterns[0] + : props.indexPatterns[0].title + ) + ); + } + + function getQueryStringInitialValue(language: string) { + const { indexPatterns, queryEnhancements } = props; + const input = queryEnhancements?.get(language)?.searchBar?.queryStringInput?.initialValue; + + if ( + !indexPatterns || + (!Array.isArray(indexPatterns) && compact(indexPatterns).length > 0) || + !input + ) + return ''; + + const defaultDataSource = indexPatterns[0]; + const dataSource = + typeof defaultDataSource === 'string' ? defaultDataSource : defaultDataSource.title; + + return input.replace('', dataSource); + } + function renderQueryInput() { if (!shouldRenderQueryInput()) return; return ( @@ -204,11 +259,15 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { disableAutoFocus={props.disableAutoFocus} indexPatterns={props.indexPatterns!} prepend={props.prepend} - query={props.query!} + query={parsedQuery} + isEnhancementsEnabled={props.isEnhancementsEnabled} + queryEnhancements={props.queryEnhancements} + settings={props.settings} screenTitle={props.screenTitle} onChange={onQueryChange} onChangeQueryInputFocus={onChangeQueryInputFocus} onSubmit={onInputSubmit} + getQueryStringInitialValue={getQueryStringInitialValue} persistedLog={persistedLog} dataTestSubj={props.dataTestSubj} /> @@ -233,10 +292,15 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { } function shouldRenderDatePicker(): boolean { - return Boolean(props.showDatePicker || props.showAutoRefreshOnly); + return Boolean( + (props.showDatePicker && (queryUiEnhancement?.showDatePicker ?? true)) ?? + (props.showAutoRefreshOnly && (queryUiEnhancement?.showAutoRefreshOnly ?? true)) + ); } function shouldRenderQueryInput(): boolean { + // TODO: MQL probably can modify to not care about index patterns + // TODO: call queryUiEnhancement?.showQueryInput return Boolean(props.showQueryInput && props.indexPatterns && props.query && storage); } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index dfa5d57411d0..da5cc0e017b2 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -42,7 +42,7 @@ import { render } from '@testing-library/react'; import { EuiTextArea } from '@elastic/eui'; -import { QueryLanguageSwitcher } from './language_switcher'; +import { LegacyQueryLanguageSwitcher } from './legacy_language_switcher'; import { QueryStringInput } from './'; import type QueryStringInputUI from './query_string_input'; @@ -50,6 +50,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../mocks'; import { stubIndexPatternWithFields } from '../../stubs'; import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; +import { SettingsMock } from '../settings/mocks'; const startMock = coreMock.createStart(); @@ -84,6 +85,8 @@ const createMockStorage = () => ({ clear: jest.fn(), }); +const settingsMock = new SettingsMock(createMockStorage(), new Map()); + function wrapQueryStringInputInContext(testProps: any, storage?: any) { const defaultOptions = { screenTitle: 'Another Screen', @@ -132,7 +135,7 @@ describe('QueryStringInput', () => { indexPatterns: [stubIndexPatternWithFields], }) ); - expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); + expect(component.find(LegacyQueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); }); it('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => { @@ -173,16 +176,17 @@ describe('QueryStringInput', () => { indexPatterns: [stubIndexPatternWithFields], disableAutoFocus: true, appName: 'discover', + settings: settingsMock, }, mockStorage ) ); - component.find(QueryLanguageSwitcher).props().onSelectLanguage('lucene'); - expect(mockStorage.set).toHaveBeenCalledWith( - 'opensearchDashboards.userQueryLanguage', - 'lucene' - ); + component.find(LegacyQueryLanguageSwitcher).props().onSelectLanguage('lucene'); + expect(settingsMock.updateSettings).toHaveBeenCalledWith({ + userQueryLanguage: 'lucene', + userQueryString: '', + }); expect(mockCallback).toHaveBeenCalledWith({ query: '', language: 'lucene' }); }); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 5d071748700c..db0d732d1db6 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -47,7 +47,7 @@ import { import { FormattedMessage } from '@osd/i18n/react'; import { debounce, compact, isEqual, isFunction } from 'lodash'; import { Toast } from 'src/core/public'; -import { IDataPluginServices, IIndexPattern, Query } from '../..'; +import { IDataPluginServices, IIndexPattern, Query, TimeRange } from '../..'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; import { @@ -56,13 +56,18 @@ import { } from '../../../../opensearch_dashboards_react/public'; import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; +import { LegacyQueryLanguageSwitcher } from './legacy_language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; import { SuggestionsListSize } from '../typeahead/suggestions_component'; -import { SuggestionsComponent } from '..'; +import { Settings, SuggestionsComponent } from '..'; +import { DataSettings, QueryEnhancement } from '../types'; export interface QueryStringInputProps { indexPatterns: Array; query: Query; + isEnhancementsEnabled?: boolean; + queryEnhancements?: Map; + settings?: Settings; disableAutoFocus?: boolean; screenTitle?: string; prepend?: any; @@ -71,9 +76,10 @@ export interface QueryStringInputProps { placeholder?: string; languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; onBlur?: () => void; - onChange?: (query: Query) => void; + onChange?: (query: Query, dateRange?: TimeRange) => void; onChangeQueryInputFocus?: (isFocused: boolean) => void; - onSubmit?: (query: Query) => void; + onSubmit?: (query: Query, dateRange?: TimeRange) => void; + getQueryStringInitialValue?: (language: string) => string; dataTestSubj?: string; size?: SuggestionsListSize; className?: string; @@ -108,6 +114,7 @@ const KEY_CODES = { }; // Needed for React.lazy +// TODO: MQL export this and let people extended this // eslint-disable-next-line import/no-default-export export default class QueryStringInputUI extends Component { public state: State = { @@ -130,9 +137,13 @@ export default class QueryStringInputUI extends Component { private queryBarInputDivRefInstance: RefObject = createRef(); private getQueryString = () => { + if (!this.props.query.query) { + return this.props.getQueryStringInitialValue?.(this.props.query.language) ?? ''; + } return toUser(this.props.query.query); }; + // TODO: MQL don't do this here? || Fetch data sources private fetchIndexPatterns = async () => { const stringPatterns = this.props.indexPatterns.filter( (indexPattern) => typeof indexPattern === 'string' @@ -224,7 +235,7 @@ export default class QueryStringInputUI extends Component { } }, 100); - private onSubmit = (query: Query) => { + private onSubmit = (query: Query, dateRange?: TimeRange) => { if (this.props.onSubmit) { if (this.persistedLog) { this.persistedLog.add(query.query); @@ -234,11 +245,11 @@ export default class QueryStringInputUI extends Component { } }; - private onChange = (query: Query) => { + private onChange = (query: Query, dateRange?: TimeRange) => { this.updateSuggestions(); if (this.props.onChange) { - this.props.onChange({ query: fromUser(query.query), language: query.language }); + this.props.onChange({ query: fromUser(query.query), language: query.language }, dateRange); } }; @@ -457,6 +468,7 @@ export default class QueryStringInputUI extends Component { } }; + // TODO: MQL consider moving language select language of setting search source here private onSelectLanguage = (language: string) => { // Send telemetry info every time the user opts in or out of kuery // As a result it is important this function only ever gets called in the @@ -465,11 +477,28 @@ export default class QueryStringInputUI extends Component { body: JSON.stringify({ opt_in: language === 'kuery' }), }); - this.services.storage.set('opensearchDashboards.userQueryLanguage', language); + const newQuery = { + query: this.props.getQueryStringInitialValue?.(language) ?? '', + language, + }; - const newQuery = { query: '', language }; - this.onChange(newQuery); - this.onSubmit(newQuery); + const fields = this.props.queryEnhancements?.get(newQuery.language)?.fields; + const newSettings: DataSettings = { + userQueryLanguage: newQuery.language, + userQueryString: newQuery.query, + ...(fields && { uiOverrides: { fields } }), + }; + this.props.settings?.updateSettings(newSettings); + + const dateRangeEnhancement = this.props.queryEnhancements?.get(language)?.searchBar?.dateRange; + const dateRange = dateRangeEnhancement + ? { + from: dateRangeEnhancement.initialFrom!, + to: dateRangeEnhancement.initialTo!, + } + : undefined; + this.onChange(newQuery, dateRange); + this.onSubmit(newQuery, dateRange); }; private onOutsideClick = () => { @@ -619,6 +648,14 @@ export default class QueryStringInputUI extends Component { return (
{this.props.prepend} + {!!this.props.isEnhancementsEnabled && ( + + )}
{
- - + {!!!this.props.isEnhancementsEnabled && ( + + )}
); } diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index c739b955ff19..b2fdea2b49c9 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -41,11 +41,15 @@ import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; import { Filter, Query, TimeRange } from '../../../common'; import { useQueryStringManager } from './lib/use_query_string_manager'; +import { QueryEnhancement, Settings } from '../types'; interface StatefulSearchBarDeps { core: CoreStart; data: Omit; storage: IStorageWrapper; + isEnhancementsEnabled: boolean; + queryEnhancements: Map; + settings: Settings; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -130,11 +134,19 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) { +export function createSearchBar({ + core, + storage, + data, + isEnhancementsEnabled, + queryEnhancements, + settings, +}: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { const { useDefaultBehaviors } = props; + // Handle queries const onQuerySubmitRef = useRef(props.onQuerySubmit); @@ -148,6 +160,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) query: props.query, queryStringManager: data.query.queryString, }); + const { timeRange, refreshInterval } = useTimefilter({ dateRangeFrom: props.dateRangeFrom, dateRangeTo: props.dateRangeTo, @@ -201,6 +214,9 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) isRefreshPaused={refreshInterval.pause} filters={filters} query={query} + isEnhancementsEnabled={isEnhancementsEnabled} + queryEnhancements={queryEnhancements} + settings={settings} onFiltersUpdated={defaultFiltersUpdated(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)} savedQuery={savedQuery} diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index b05b18b6d64e..937da74914a0 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -47,6 +47,7 @@ import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; import { FilterBar } from '../filter_bar/filter_bar'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { SavedQueryManagementComponent } from '../saved_query_management'; +import { QueryEnhancement, Settings } from '../types'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -78,6 +79,9 @@ export interface SearchBarOwnProps { dateRangeTo?: string; // Query bar - should be in SearchBarInjectedDeps query?: Query; + isEnhancementsEnabled?: boolean; + queryEnhancements?: Map; + settings?: Settings; // Show when user has privileges to save showSaveQuery?: boolean; savedQuery?: SavedQuery; @@ -96,6 +100,7 @@ export interface SearchBarOwnProps { export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; +// TODO: MQL: include query enhancement in state in case make adding data sources at runtime? interface State { isFiltersVisible: boolean; showSaveQueryModal: boolean; @@ -202,6 +207,7 @@ class SearchBarUI extends Component { }; private shouldRenderQueryBar() { + // TODO: MQL handle no index patterns? const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly; const showQueryInput = this.props.showQueryInput && this.props.indexPatterns && this.state.query; @@ -209,11 +215,14 @@ class SearchBarUI extends Component { } private shouldRenderFilterBar() { + // TODO: MQL handle no index patterns? return ( this.props.showFilterBar && this.props.filters && this.props.indexPatterns && - compact(this.props.indexPatterns).length > 0 + compact(this.props.indexPatterns).length > 0 && + (this.props.queryEnhancements?.get(this.state.query?.language!)?.searchBar?.showFilterBar ?? + true) ); } @@ -393,9 +402,13 @@ class SearchBarUI extends Component { let queryBar; if (this.shouldRenderQueryBar()) { + // TODO: MQL make this default query bar top row but this.props.queryEnhancements.get(language) can pass a component queryBar = ( + ) {} + + getUserQueryLanguage() { + return this.storage.get('opensearchDashboards.userQueryLanguage') || 'kuery'; + } + + setUserQueryLanguage(language: string) { + this.storage.set('opensearchDashboards.userQueryLanguage', language); + return true; + } + + getUserQueryString() { + return this.storage.get('opensearchDashboards.userQueryString') || ''; + } + + setUserQueryString(query: string) { + this.storage.set('opensearchDashboards.userQueryString', query); + return true; + } + + getUiOverrides() { + return this.storage.get('opensearchDashboards.uiOverrides') || {}; + } + + setUiOverrides(overrides?: { [key: string]: any }) { + if (!overrides) { + this.storage.remove('opensearchDashboards.uiOverrides'); + setFieldOverrides(undefined); + return true; + } + this.storage.set('opensearchDashboards.uiOverrides', overrides); + setFieldOverrides(overrides.fields); + return true; + } + + setUiOverridesByUserQueryLanguage(language: string) { + const queryEnhancement = this.queryEnhancements.get(language); + if (queryEnhancement) { + const { fields = {}, showDocLinks } = queryEnhancement; + this.setUiOverrides({ fields, showDocLinks }); + } else { + this.setUiOverrides({ fields: undefined, showDocLinks: undefined }); + } + } + + toJSON(): DataSettings { + return { + userQueryLanguage: this.getUserQueryLanguage(), + userQueryString: this.getUserQueryString(), + uiOverrides: this.getUiOverrides(), + }; + } + + updateSettings({ userQueryLanguage, userQueryString, uiOverrides }: DataSettings) { + this.setUserQueryLanguage(userQueryLanguage); + this.setUserQueryString(userQueryString); + this.setUiOverrides(uiOverrides); + } +} + +interface Deps { + storage: IStorageWrapper; + queryEnhancements: Map; +} + +export function createSettings({ storage, queryEnhancements }: Deps) { + return new Settings(storage, queryEnhancements); +} diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts new file mode 100644 index 000000000000..464e6a97afde --- /dev/null +++ b/src/plugins/data/public/ui/types.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SearchInterceptor } from '../search'; +import { IndexPatternSelectProps } from './index_pattern_select'; +import { StatefulSearchBarProps } from './search_bar'; +import { Settings } from './settings'; + +export * from './settings'; + +export interface QueryEnhancement { + // TODO: MQL do want to default have supported all data_sources? + // or should data connect have a record of query enhancements that are supported + language: string; + search: SearchInterceptor; + // Leave blank to support all data sources + // supportedDataSourceTypes?: Record; + searchBar?: { + showQueryInput?: boolean; + showFilterBar?: boolean; + showDatePicker?: boolean; + showAutoRefreshOnly?: boolean; + queryStringInput?: { + // will replace '' with the data source name + initialValue?: string; + }; + dateRange?: { + initialFrom?: string; + initialTo?: string; + }; + }; + fields?: { + filterable?: boolean; + visualizable?: boolean; + }; + showDocLinks?: boolean; + // List of supported app names that this enhancement should be enabled for, + // if not provided it will be enabled for all apps + supportedAppNames?: string[]; +} + +export interface UiEnhancements { + query?: QueryEnhancement; +} + +/** + * The setup contract exposed by the Search plugin exposes the search strategy extension + * point. + */ +export interface IUiSetup { + __enhance: (enhancements: UiEnhancements) => void; +} + +/** + * Data plugin prewired UI components + */ +export interface IUiStart { + isEnhancementsEnabled: boolean; + queryEnhancements: Map; + IndexPatternSelect: React.ComponentType; + SearchBar: React.ComponentType; + Settings: Settings; +} diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts new file mode 100644 index 000000000000..1ef834b54564 --- /dev/null +++ b/src/plugins/data/public/ui/ui_service.ts @@ -0,0 +1,70 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { IUiStart, IUiSetup, QueryEnhancement, UiEnhancements } from './types'; + +import { ConfigSchema } from '../../config'; +import { createIndexPatternSelect } from './index_pattern_select'; +import { createSearchBar } from './search_bar/create_search_bar'; +import { createSettings } from './settings'; +import { DataPublicPluginStart } from '../types'; +import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public'; + +/** @internal */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UiServiceSetupDependencies {} + +/** @internal */ +export interface UiServiceStartDependencies { + dataServices: Omit; + storage: IStorageWrapper; +} + +export class UiService implements Plugin { + enhancementsConfig: ConfigSchema['enhancements']; + private queryEnhancements: Map = new Map(); + + constructor(initializerContext: PluginInitializerContext) { + const { enhancements } = initializerContext.config.get(); + + this.enhancementsConfig = enhancements; + } + + public setup(core: CoreSetup, {}: UiServiceSetupDependencies): IUiSetup { + return { + __enhance: (enhancements?: UiEnhancements) => { + if (!enhancements) return; + if (!this.enhancementsConfig.enabled) return; + if (enhancements.query && enhancements.query.language) { + this.queryEnhancements.set(enhancements.query.language, enhancements.query); + } + }, + }; + } + + public start(core: CoreStart, { dataServices, storage }: UiServiceStartDependencies): IUiStart { + const Settings = createSettings({ storage, queryEnhancements: this.queryEnhancements }); + + const SearchBar = createSearchBar({ + core, + data: dataServices, + storage, + isEnhancementsEnabled: this.enhancementsConfig?.enabled, + queryEnhancements: this.queryEnhancements, + settings: Settings, + }); + + return { + isEnhancementsEnabled: this.enhancementsConfig?.enabled, + queryEnhancements: this.queryEnhancements, + IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), + SearchBar, + Settings, + }; + } + + public stop() {} +} diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 4bc3ad62a4ae..5fe531729283 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -133,11 +133,13 @@ export { IFieldFormatsRegistry, FieldFormatsGetConfigFn, FieldFormatConfig } fro * Index patterns: */ -import { isNestedField, isFilterable } from '../common'; +import { isNestedField, isFilterable, setOverrides, getOverrides } from '../common'; export const indexPatterns = { isFilterable, isNestedField, + setOverrides, + getOverrides, }; export { @@ -272,6 +274,8 @@ export const search = { export { // osd field types castOpenSearchToOsdFieldTypeName, + getOsdFieldOverrides, + setOsdFieldOverrides, // query Filter, getTime, @@ -300,6 +304,7 @@ export { export const config: PluginConfigDescriptor = { exposeToBrowser: { + enhancements: true, autocomplete: true, search: true, }, diff --git a/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts b/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts index d7cbd48a6507..172acf9f0e2e 100644 --- a/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts +++ b/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts @@ -45,10 +45,14 @@ export async function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient const maxConcurrentShardRequests = await uiSettingsClient.get( UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS ); + const dataFrameHydrationStrategy = await uiSettingsClient.get( + UI_SETTINGS.DATAFRAME_HYDRATION_STRATEGY + ); return { maxConcurrentShardRequests: maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined, ignoreThrottled, + dataFrameHydrationStrategy, ignoreUnavailable: true, // Don't fail if the index/indices don't exist trackTotalHits: true, }; diff --git a/src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts b/src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts index fa1b3e4da94c..c5c7602bc4f9 100644 --- a/src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts +++ b/src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts @@ -64,8 +64,12 @@ export const opensearchSearchStrategyProvider = ( throw new Error(`Unsupported index pattern type ${request.indexType}`); } - // ignoreThrottled is not supported in OSS - const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams(uiSettingsClient); + // ignoreThrottled & dataFrameHydrationStrategy is not supported by default + const { + ignoreThrottled, + dataFrameHydrationStrategy, + ...defaultParams + } = await getDefaultSearchParams(uiSettingsClient); const params = toSnakeCase({ ...defaultParams, diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts index dc257620fcca..b6bca0875d40 100644 --- a/src/plugins/data/server/search/routes/call_msearch.ts +++ b/src/plugins/data/server/search/routes/call_msearch.ts @@ -80,8 +80,12 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) { const config = await globalConfig$.pipe(first()).toPromise(); const timeout = getShardTimeout(config); - // trackTotalHits is not supported by msearch - const { trackTotalHits, ...defaultParams } = await getDefaultSearchParams(uiSettings); + // trackTotalHits and dataFrameHydrationStrategy is not supported by msearch + const { + trackTotalHits, + dataFrameHydrationStrategy, + ...defaultParams + } = await getDefaultSearchParams(uiSettings); const body = convertRequestBody(params.body, timeout); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index b955596922a0..2eef461b94da 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -73,6 +73,13 @@ import { } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; +import { + DataFrameService, + IDataFrame, + IDataFrameResponse, + createDataFrameCache, + dataFrameToSpec, +} from '../../common'; type StrategyMap = Record>; @@ -98,6 +105,7 @@ export interface SearchRouteDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); private readonly searchSourceService = new SearchSourceService(); + private readonly dfCache = createDataFrameCache(); private defaultSearchStrategyName: string = OPENSEARCH_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; @@ -166,7 +174,8 @@ export class SearchService implements Plugin { }); return { - __enhance: (enhancements: SearchEnhancements) => { + __enhance: (enhancements?: SearchEnhancements) => { + if (!enhancements) return; if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { this.defaultSearchStrategyName = enhancements.defaultStrategy; } @@ -203,6 +212,29 @@ export class SearchService implements Plugin { searchSourceRequiredUiSettings ); + const dfService: DataFrameService = { + get: () => this.dfCache.get(), + set: async (dataFrame: IDataFrame) => { + if (this.dfCache.get() && this.dfCache.get()?.name !== dataFrame.name) { + scopedIndexPatterns.clearCache(this.dfCache.get()!.name, false); + } + this.dfCache.set(dataFrame); + const existingIndexPattern = scopedIndexPatterns.getByTitle(dataFrame.name!, true); + const dataSet = await scopedIndexPatterns.create( + dataFrameToSpec(dataFrame, existingIndexPattern?.id), + !existingIndexPattern?.id + ); + // save to cache by title because the id is not unique for temporary index pattern created + scopedIndexPatterns.saveToCache(dataSet.title, dataSet); + }, + clear: () => { + if (this.dfCache.get() === undefined) return; + // name because the id is not unique for temporary index pattern created + scopedIndexPatterns.clearCache(this.dfCache.get()!.name, false); + this.dfCache.clear(); + }, + }; + const searchSourceDependencies: SearchSourceDependencies = { getConfig: (key: string): T => uiSettingsCache[key], search: (searchRequest, options) => { @@ -237,6 +269,7 @@ export class SearchService implements Plugin { }), loadingCount$: new BehaviorSubject(0), }, + df: dfService, }; return this.searchSourceService.start(scopedIndexPatterns, searchSourceDependencies); @@ -251,7 +284,9 @@ export class SearchService implements Plugin { private registerSearchStrategy = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( name: string, strategy: ISearchStrategy @@ -261,7 +296,9 @@ export class SearchService implements Plugin { private search = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( context: RequestHandlerContext, searchRequest: SearchStrategyRequest, @@ -274,7 +311,9 @@ export class SearchService implements Plugin { private getSearchStrategy = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( name: string ): ISearchStrategy => { diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 75f21d39c0bf..6927d1289673 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -38,6 +38,7 @@ import { import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors'; import { IOpenSearchSearchRequest, IOpenSearchSearchResponse } from './opensearch_search'; +import { IDataFrameResponse } from '../../common'; export interface SearchEnhancements { defaultStrategy: string; @@ -51,7 +52,9 @@ export interface ISearchSetup { */ registerSearchStrategy: < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( name: string, strategy: ISearchStrategy @@ -96,7 +99,9 @@ export interface ISearchStart< */ export interface ISearchStrategy< SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse > { search: ( context: RequestHandlerContext, diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 77f4afd11887..e12113b87b29 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -55,6 +55,21 @@ const requestPreferenceOptionLabels = { }), }; +const dataFrameHydrationStrategyOptionLabels = { + perSource: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyPerSourceText', { + defaultMessage: 'On data source change', + }), + perQuery: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyPerQueryText', { + defaultMessage: 'Per query', + }), + perResponse: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyPerResponseText', { + defaultMessage: 'Per response', + }), + advanced: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyAdvancedText', { + defaultMessage: 'Advanced', + }), +}; + // We add the `en` key manually here, since that's not a real numeral locale, but the // default fallback in case the locale is not found. const numeralLanguageIds = [ @@ -690,5 +705,47 @@ export function getUiSettings(): Record> { }), schema: schema.boolean(), }, + [UI_SETTINGS.DATAFRAME_HYDRATION_STRATEGY]: { + name: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyTitle', { + defaultMessage: 'Data frame hydration strategy', + }), + value: 'perSource', + options: ['perSource', 'perQuery'], + optionLabels: dataFrameHydrationStrategyOptionLabels, + type: 'select', + description: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyText', { + defaultMessage: `Allows you to set how often the data frame schema is updated. +
    +
  • {perSource}: hydrates the schema when the data source changes. + For example, any time the index pattern is change the data frame schema is hydrated.
  • +
  • {perQuery}: hydrates the schema per query to the data source. + Could be expensive, but ensures the schema of the data frame fits the result set.
  • +
  • {perResponse}: hydrates the schema if the data source returns a schema. + Not Implemented.
  • +
  • {advanced}: hydrates the schema in intervals. If the schema hasn't changed the interval increases. + If the schema has changed the interval resets. Not Implemented.
  • +
`, + values: { + perSource: dataFrameHydrationStrategyOptionLabels.perSource, + perQuery: dataFrameHydrationStrategyOptionLabels.perQuery, + perResponse: dataFrameHydrationStrategyOptionLabels.perResponse, + advanced: dataFrameHydrationStrategyOptionLabels.advanced, + }, + }), + category: ['search'], + schema: schema.string(), + }, + [UI_SETTINGS.QUERY_DATA_SOURCE_READONLY]: { + name: i18n.translate('data.advancedSettings.query.dataSourceReadOnlyTitle', { + defaultMessage: 'Read-only data source in query bar', + }), + value: true, + description: i18n.translate('data.advancedSettings.query.dataSourceReadOnlyText', { + defaultMessage: + 'When enabled, the global search bar prevents modifying the data source in the query input. ' + + '
Experimental: Setting to false enables modifying the data source.', + }), + schema: schema.boolean(), + }, }; } diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx index ba80e719491f..496d80703d7c 100644 --- a/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx @@ -80,8 +80,8 @@ export function computeVisibleColumns( const timeFieldName = idxPattern.timeFieldName; let visibleColumnNames = columnNames; - if (displayTimeColumn && !columnNames.includes(timeFieldName)) { - visibleColumnNames = [timeFieldName, ...columnNames]; + if (displayTimeColumn && !columnNames.includes(timeFieldName!)) { + visibleColumnNames = [timeFieldName!, ...columnNames]; } return visibleColumnNames; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_data_frame.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_data_frame.tsx new file mode 100644 index 000000000000..26260635a303 --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_data_frame.tsx @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; + +export interface Props { + onCreateIndexPattern: () => void; + onNormalizeIndexPattern: () => void; +} + +export function DiscoverFieldDataFrame({ onCreateIndexPattern, onNormalizeIndexPattern }: Props) { + return ( + + + + {i18n.translate('discover.fieldChooser.dataFrame.normalizeIndexPattern', { + defaultMessage: 'Normalize', + })} + + + + + {i18n.translate('discover.fieldChooser.dataFrame.createIndexPattern', { + defaultMessage: 'Create index pattern', + })} + + + + ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 6fee8dde6b60..ed5c8b1773f2 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -98,6 +98,8 @@ function getCompProps(): DiscoverSidebarProps { onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), + onNormalize: jest.fn(), + onCreateIndexPattern: jest.fn(), selectedIndexPattern: indexPattern, onReorderFields: jest.fn(), }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 9dcb5bf337cb..9d69f756f0e4 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -43,6 +43,7 @@ import { import { I18nProvider } from '@osd/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverFieldSearch } from './discover_field_search'; +import { DiscoverFieldDataFrame } from './discover_field_data_frame'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public'; @@ -51,6 +52,7 @@ import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; import { getServices } from '../../../opensearch_dashboards_services'; import { FieldDetails } from './types'; +import { displayIndexPatternCreation } from './lib/display_index_pattern_creation'; export interface DiscoverSidebarProps { /** @@ -82,6 +84,14 @@ export interface DiscoverSidebarProps { * @param fieldName */ onRemoveField: (fieldName: string) => void; + /** + * Callback function to create an index pattern + */ + onCreateIndexPattern: () => void; + /** + * Callback function to normalize the index pattern + */ + onNormalize: () => void; /** * Currently selected index pattern */ @@ -89,7 +99,16 @@ export interface DiscoverSidebarProps { } export function DiscoverSidebar(props: DiscoverSidebarProps) { - const { columns, fieldCounts, hits, onAddField, onReorderFields, selectedIndexPattern } = props; + const { + columns, + fieldCounts, + hits, + onAddField, + onReorderFields, + onNormalize, + onCreateIndexPattern, + selectedIndexPattern, + } = props; const [fields, setFields] = useState(null); const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); const services = useMemo(() => getServices(), []); @@ -99,6 +118,12 @@ export function DiscoverSidebar(props: DiscoverSidebarProps) { setFields(newFields); }, [selectedIndexPattern, fieldCounts, hits, services]); + const onNormalizeIndexPattern = useCallback(async () => { + await onNormalize(); + const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts); + setFields(newFields); + }, [fieldCounts, onNormalize, selectedIndexPattern]); + const onChangeFieldSearch = useCallback( (field: string, value: string | boolean | undefined) => { const newState = setFieldFilterProp(fieldFilterState, field, value); @@ -195,6 +220,14 @@ export function DiscoverSidebar(props: DiscoverSidebarProps) { types={fieldTypes} /> + {displayIndexPatternCreation(selectedIndexPattern) ? ( + + + + ) : null} {fields.length > 0 && ( <> diff --git a/src/plugins/discover/public/application/components/sidebar/lib/display_index_pattern_creation.tsx b/src/plugins/discover/public/application/components/sidebar/lib/display_index_pattern_creation.tsx new file mode 100644 index 000000000000..e7dccc08ef37 --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/lib/display_index_pattern_creation.tsx @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPattern, DATA_FRAME_TYPES } from '../../../../../../data/public'; + +/** + * if we should display index pattern creation in the sidebar + */ +export function displayIndexPatternCreation(indexPattern: IndexPattern | undefined): boolean { + if (!indexPattern || !indexPattern.type || !indexPattern.id) return false; + return ( + Object.values(DATA_FRAME_TYPES).includes(indexPattern.type as DATA_FRAME_TYPES) && + Object.values(DATA_FRAME_TYPES).includes(indexPattern.id as DATA_FRAME_TYPES) + ); +} diff --git a/src/plugins/discover/public/application/helpers/get_data_set.ts b/src/plugins/discover/public/application/helpers/get_data_set.ts new file mode 100644 index 000000000000..b0431ac31c1e --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_data_set.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; +import { SearchData } from '../view_components/utils/use_search'; + +function getDataSet( + indexPattern: IndexPattern | undefined, + state: SearchData, + indexPatternsService: IndexPatternsContract +) { + if (!indexPattern) { + return; + } + return ( + (state.title && + state.title !== indexPattern?.title && + indexPatternsService.getByTitle(state.title!, true)) || + indexPattern + ); +} + +export { getDataSet }; diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx index 6b4cd2a87c91..fbe180f7e316 100644 --- a/src/plugins/discover/public/application/view_components/panel/index.tsx +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -20,6 +20,7 @@ import { IndexPatternField, opensearchFilters } from '../../../../../data/public import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverViewServices } from '../../../build_services'; import { popularizeField } from '../../helpers/popularize_field'; +import { getDataSet } from '../../helpers/get_data_set'; import { buildColumns } from '../../utils/columns'; // eslint-disable-next-line import/no-default-export @@ -31,6 +32,7 @@ export default function DiscoverPanel(props: ViewProps) { }, capabilities, indexPatterns, + application, } = services; const { data$, indexPattern } = useDiscoverContext(); const [fetchState, setFetchState] = useState(data$.getValue()); @@ -86,14 +88,30 @@ export default function DiscoverPanel(props: ViewProps) { [filterManager, indexPattern] ); + const onCreateIndexPattern = useCallback(async () => { + if (!fetchState.title) return; + if (fetchState.title === indexPattern?.title) return; + application?.navigateToApp('management', { + path: `opensearch-dashboards/indexPatterns/create?id=${fetchState.title}`, + }); + }, [application, fetchState.title, indexPattern?.title]); + + const onNormalize = useCallback(async () => { + if (!fetchState.title) return; + if (fetchState.title === indexPattern?.title) return; + const dataSet = getDataSet(indexPattern, fetchState, indexPatterns); + await indexPatterns.refreshFields(dataSet!, true); + }, [fetchState, indexPattern, indexPatterns]); + return ( { - if (indexPattern && capabilities.discover?.save) { - popularizeField(indexPattern, fieldName, indexPatterns); + const dataSet = getDataSet(indexPattern, fetchState, indexPatterns); + if (dataSet && capabilities.discover?.save) { + popularizeField(dataSet, fieldName, indexPatterns); } dispatch( @@ -104,8 +122,9 @@ export default function DiscoverPanel(props: ViewProps) { ); }} onRemoveField={(fieldName) => { - if (indexPattern && capabilities.discover?.save) { - popularizeField(indexPattern, fieldName, indexPatterns); + const dataSet = getDataSet(indexPattern, fetchState, indexPatterns); + if (dataSet && capabilities.discover?.save) { + popularizeField(dataSet, fieldName, indexPatterns); } dispatch(removeColumn(fieldName)); @@ -118,7 +137,9 @@ export default function DiscoverPanel(props: ViewProps) { }) ); }} - selectedIndexPattern={indexPattern} + selectedIndexPattern={getDataSet(indexPattern, fetchState, indexPatterns)} + onCreateIndexPattern={onCreateIndexPattern} + onNormalize={onNormalize} onAddFilter={onAddFilter} /> ); diff --git a/src/plugins/discover/public/application/view_components/utils/update_search_source.ts b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts index 1404773eb9d4..a8480fdad18a 100644 --- a/src/plugins/discover/public/application/view_components/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts @@ -30,9 +30,22 @@ export const updateSearchSource = async ({ histogramConfigs, }: Props) => { const { uiSettings, data } = services; + let dataSet = indexPattern; + const dataFrame = searchSource?.getDataFrame(); + if ( + searchSource && + dataFrame && + dataFrame.name && + dataFrame.name !== '' && + dataSet.title !== dataFrame.name + ) { + dataSet = data.indexPatterns.getByTitle(dataFrame.name, true) ?? dataSet; + searchSource.setField('index', dataSet); + } + const sortForSearchSource = getSortForSearchSource( sort, - indexPattern, + dataSet, uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ); const size = uiSettings.get(SAMPLE_SIZE_SETTING); @@ -43,18 +56,18 @@ export const updateSearchSource = async ({ // searchSource which applies time range const timeRangeSearchSource = await data.search.searchSource.create(); const { isDefault } = indexPatternUtils; - if (isDefault(indexPattern)) { + if (isDefault(dataSet)) { const timefilter = data.query.timefilter.timefilter; timeRangeSearchSource.setField('filter', () => { - return timefilter.createFilter(indexPattern); + return timefilter.createFilter(dataSet); }); } searchSourceInstance.setParent(timeRangeSearchSource); searchSourceInstance.setFields({ - index: indexPattern, + index: dataSet, sort: sortForSearchSource, size, query: data.query.queryString.getQuery() || null, diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index 06eabb1e139f..9da9704f32a7 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -47,6 +47,7 @@ export interface SearchData { rows?: OpenSearchSearchHit[]; bucketInterval?: TimechartHeaderBucketInterval | {}; chartData?: Chart; + title?: string; } export type SearchRefetch = 'refetch' | undefined; @@ -105,7 +106,8 @@ export const useSearch = (services: DiscoverViewServices) => { const refetch$ = useMemo(() => new Subject(), []); const fetch = useCallback(async () => { - if (!indexPattern) { + let dataSet = indexPattern; + if (!dataSet) { data$.next({ status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, }); @@ -122,17 +124,19 @@ export const useSearch = (services: DiscoverViewServices) => { // Abort any in-progress requests before fetching again if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort(); fetchStateRef.current.abortController = new AbortController(); - const histogramConfigs = indexPattern.timeFieldName - ? createHistogramConfigs(indexPattern, interval || 'auto', data) + const histogramConfigs = dataSet.timeFieldName + ? createHistogramConfigs(dataSet, interval || 'auto', data) : undefined; const searchSource = await updateSearchSource({ - indexPattern, + indexPattern: dataSet, services, sort, searchSource: savedSearch?.searchSource, histogramConfigs, }); + dataSet = searchSource.getField('index'); + try { // Only show loading indicator if we are fetching when the rows are empty if (fetchStateRef.current.rows?.length === 0) { @@ -149,7 +153,7 @@ export const useSearch = (services: DiscoverViewServices) => { }); const inspectorRequest = inspectorAdapters.requests.start(title, { description }); inspectorRequest.stats(getRequestInspectorStats(searchSource)); - searchSource.getSearchRequestBody().then((body) => { + searchSource.getSearchRequestBody().then((body: object) => { inspectorRequest.json(body); }); @@ -167,7 +171,7 @@ export const useSearch = (services: DiscoverViewServices) => { let bucketInterval = {}; let chartData; for (const row of rows) { - const fields = Object.keys(indexPattern.flattenHit(row)); + const fields = Object.keys(dataSet!.flattenHit(row)); for (const fieldName of fields) { fetchStateRef.current.fieldCounts[fieldName] = (fetchStateRef.current.fieldCounts[fieldName] || 0) + 1; @@ -196,6 +200,10 @@ export const useSearch = (services: DiscoverViewServices) => { rows, bucketInterval, chartData, + title: + indexPattern?.title !== searchSource.getDataFrame()?.name + ? searchSource.getDataFrame()?.name + : indexPattern?.title, }); } catch (error) { // If the request was aborted then no need to surface this error in the UI diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index f8e0f254f925..8b46889a8e36 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -202,6 +202,7 @@ export class DiscoverPlugin generateCb: (renderProps: any) => { const globalFilters: any = getServices().filterManager.getGlobalFilters(); const appFilters: any = getServices().filterManager.getAppFilters(); + const showDocLinks = getServices().data.ui.Settings.getUiOverrides().showDocLinks; const hash = stringify( url.encodeQuery({ @@ -222,7 +223,9 @@ export class DiscoverPlugin return { url: generateDocViewsUrl(contextUrl), - hide: !renderProps.indexPattern.isTimeBased(), + hide: + (showDocLinks !== undefined ? !showDocLinks : false) || + !renderProps.indexPattern.isTimeBased(), }; }, order: 1, @@ -233,11 +236,15 @@ export class DiscoverPlugin defaultMessage: 'View single document', }), generateCb: (renderProps) => { + const showDocLinks = getServices().data.ui.Settings.getUiOverrides().showDocLinks; + const docUrl = `#/doc/${renderProps.indexPattern.id}/${ renderProps.hit._index }?id=${encodeURIComponent(renderProps.hit._id)}`; + return { url: generateDocViewsUrl(docUrl), + hide: showDocLinks !== undefined ? !showDocLinks : false, }; }, order: 2, diff --git a/src/plugins/home/server/services/sample_data/data_sets/index.ts b/src/plugins/home/server/services/sample_data/data_sets/index.ts index a75115950f93..dc9ac8ae3711 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/index.ts @@ -31,3 +31,4 @@ export { flightsSpecProvider } from './flights'; export { logsSpecProvider } from './logs'; export { ecommerceSpecProvider } from './ecommerce'; +export { appendDataSourceId, getSavedObjectsWithDataSource } from './util'; diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts index 1e2951c596b9..c202290f911a 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts @@ -75,13 +75,19 @@ export const sampleDataSchema = { // saved object id of main dashboard for sample data set overviewDashboard: Joi.string().required(), + getDataSourceIntegratedDashboard: Joi.func().required(), appLinks: Joi.array().items(appLinkSchema).default([]), // saved object id of default index-pattern for sample data set defaultIndex: Joi.string().required(), + getDataSourceIntegratedDefaultIndex: Joi.func().required(), // OpenSearch Dashboards saved objects (index patter, visualizations, dashboard, ...) // Should provide a nice demo of OpenSearch Dashboards's functionality with the sample data set savedObjects: Joi.array().items(Joi.object()).required(), + getDataSourceIntegratedSavedObjects: Joi.func().required(), dataIndices: Joi.array().items(dataIndexSchema).required(), + + status: Joi.string(), + statusMsg: Joi.any(), }; diff --git a/src/plugins/vis_type_vega/public/data_model/opensearch_query_parser.ts b/src/plugins/vis_type_vega/public/data_model/opensearch_query_parser.ts index 8f5594863903..2bbd0159316c 100644 --- a/src/plugins/vis_type_vega/public/data_model/opensearch_query_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/opensearch_query_parser.ts @@ -207,7 +207,7 @@ export class OpenSearchQueryParser { if (context) { // Use dashboard context const newQuery = cloneDeep(this._filters); - if (timefield) { + if (timefield && newQuery.type !== 'unsupported') { newQuery.bool!.must!.push(body.query); } body.query = newQuery; From bead9ae827ceac80cd65f025f5d4ab0759dd6175 Mon Sep 17 00:00:00 2001 From: Suchit Sahoo <38322563+LDrago27@users.noreply.github.com> Date: Thu, 9 May 2024 17:16:32 -0700 Subject: [PATCH 16/27] Make Field Name Search Filter Case Insensitive (#6759) * Make Field Name Filter Case Insensitive Signed-off-by: Suchit Sahoo * Changeset file for PR #6759 created/updated --------- Signed-off-by: Suchit Sahoo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6759.yml | 2 ++ .../public/application/components/sidebar/lib/field_filter.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/6759.yml diff --git a/changelogs/fragments/6759.yml b/changelogs/fragments/6759.yml new file mode 100644 index 000000000000..ef7e5cddddda --- /dev/null +++ b/changelogs/fragments/6759.yml @@ -0,0 +1,2 @@ +feat: +- Make Field Name Search Filter Case Insensitive ([#6759](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6759)) \ No newline at end of file diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts index d72af29b43e0..599e546058d2 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts @@ -83,7 +83,8 @@ export function isFieldFiltered( field.type === '_source' || field.scripted || fieldCounts[field.name] > 0; - const matchName = !filterState.name || field.name.indexOf(filterState.name) !== -1; + const matchName = + !filterState.name || field.name.toLowerCase().indexOf(filterState.name.toLowerCase()) !== -1; // case insensitive matching name return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName; } From e8715b3e762efe558f03b80d1760f2aee0edd523 Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Fri, 10 May 2024 11:06:45 -0700 Subject: [PATCH 17/27] [Multiple Datasource Test] Add test for error_menu, item, data_source_multi_selectable (#6752) * add test for data_source_error_menu, data_source_item, data_source_multi_selectable Signed-off-by: yujin-emma * Changeset file for PR #6752 created/updated * add content verify in test Signed-off-by: yujin-emma --------- Signed-off-by: yujin-emma Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6752.yml | 2 + .../data_source_error_menu.test.tsx.snap | 95 +++++++++++++++++++ .../data_source_error_menu.test.tsx | 28 ++++++ .../data_source_item.test.tsx | 17 ++++ .../data_source_multi_selectable.test.tsx | 47 ++++++++- 5 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/6752.yml create mode 100644 src/plugins/data_source_management/public/components/data_source_error_menu/__snapshots__/data_source_error_menu.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/data_source_error_menu/data_source_error_menu.test.tsx diff --git a/changelogs/fragments/6752.yml b/changelogs/fragments/6752.yml new file mode 100644 index 000000000000..f43473bea2b8 --- /dev/null +++ b/changelogs/fragments/6752.yml @@ -0,0 +1,2 @@ +fix: +- Add test for data_source_error_menu, data_source_item, data_source_multi_selectable ([#6752](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6752)) \ No newline at end of file diff --git a/src/plugins/data_source_management/public/components/data_source_error_menu/__snapshots__/data_source_error_menu.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_error_menu/__snapshots__/data_source_error_menu.test.tsx.snap new file mode 100644 index 000000000000..f1f00f7c0ca8 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_error_menu/__snapshots__/data_source_error_menu.test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataSourceErrorMenu renders without crashing 1`] = ` + + + } + closePopover={[Function]} + data-test-subj="dataSourceErrorPopover" + display="inlineBlock" + hasArrow={true} + id="dataSourceErrorPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + Failed to fetch data sources + + + + + + + Refresh the page + + + + + + +`; diff --git a/src/plugins/data_source_management/public/components/data_source_error_menu/data_source_error_menu.test.tsx b/src/plugins/data_source_management/public/components/data_source_error_menu/data_source_error_menu.test.tsx new file mode 100644 index 000000000000..90a1ad1a4a0b --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_error_menu/data_source_error_menu.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { DataSourceErrorMenu } from './data_source_error_menu'; +import { coreMock } from '../../../../../core/public/mocks'; +import { render } from '@testing-library/react'; + +describe('DataSourceErrorMenu', () => { + const applicationMock = coreMock.createStart().application; + it('renders without crashing', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should toggle popover when icon button is clicked', () => { + const container = render(); + const iconButton = container.getByTestId('dataSourceErrorMenuHeaderLink'); + iconButton.click(); + expect(container.getByTestId('dataSourceErrorPopover')).toBeVisible(); + expect(container.getByTestId('datasourceTableEmptyState')).toHaveTextContent( + 'Failed to fetch data sources' + ); + }); +}); diff --git a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.test.tsx b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.test.tsx index 8cb736f2fb61..49c6c34a84f6 100644 --- a/src/plugins/data_source_management/public/components/data_source_item/data_source_item.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_item/data_source_item.test.tsx @@ -43,4 +43,21 @@ describe('Test on ShowDataSourceOption', () => { expect(component.find(EuiFlexItem)).toHaveLength(2); expect(component.find(EuiBadge)).toHaveLength(1); }); + + it('should render the component with a default data source', () => { + const item: DataSourceOption = { + id: 'default data source', + label: 'DataSource 1', + visible: true, + }; + const defaultDataSource = 'default data source'; + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiFlexGroup).exists()).toBe(true); + expect(wrapper.find(EuiFlexItem).exists()).toBe(true); + expect(wrapper.find(EuiBadge).exists()).toBe(true); + expect(wrapper.find(EuiBadge).prop('children')).toBe('Default'); + }); }); diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.test.tsx b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.test.tsx index 957080dbd1ab..7a9f1f96ed0c 100644 --- a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.test.tsx @@ -4,13 +4,13 @@ */ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; -import { notificationServiceMock } from '../../../../../core/public/mocks'; +import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../../core/public/mocks'; import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls, mockManagementPlugin, } from '../../mocks'; -import { ShallowWrapper, shallow } from 'enzyme'; +import { ShallowWrapper, mount, shallow } from 'enzyme'; import { DataSourceMultiSelectable } from './data_source_multi_selectable'; import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; @@ -91,6 +91,11 @@ describe('DataSourceMultiSelectable', () => { ); await nextTick(); expect(toasts.add).toBeCalledTimes(1); + expect(toasts.add).toBeCalledWith({ + color: 'danger', + text: expect.any(Function), + title: 'Failed to fetch data sources', + }); }); it('should callback when onChange happens', async () => { @@ -111,7 +116,7 @@ describe('DataSourceMultiSelectable', () => { expect(callbackMock).toBeCalledWith([]); }); - it('should retrun correct state when ui Settings provided', async () => { + it('should return correct state when ui Settings provided', async () => { spyOn(uiSettings, 'get').and.returnValue('test1'); component = shallow( { expect(component.state('selectedOptions')).toHaveLength(3); }); - it('should retrun correct state when ui Settings provided and hide cluster is false', async () => { + it('should return correct state when ui Settings provided and hide cluster is false', async () => { spyOn(uiSettings, 'get').and.returnValue('test1'); component = shallow( { expect(component.state('defaultDataSource')).toEqual('test1'); expect(component.state('selectedOptions')).toHaveLength(4); }); + + it('should handle no available data source error when selected option is empty and hide localcluster', async () => { + mockResponseForSavedObjectsCalls(client, 'find', {}); + const wrapper = mount( + + ); + await wrapper.instance().componentDidMount!(); + expect(wrapper.state('selectedOptions')).toHaveLength(0); + expect(wrapper.state('showEmptyState')).toBe(true); + }); + + it('should not handle no available data source error when selected option is empty and not hide localcluster', async () => { + mockResponseForSavedObjectsCalls(client, 'find', {}); + const wrapper = mount( + + ); + await wrapper.instance().componentDidMount!(); + expect(wrapper.state('selectedOptions')).toHaveLength(1); + expect(wrapper.state('showEmptyState')).toBe(false); + }); }); From 1beabfb0e9c679a4771326d53a7a6eb946b039b9 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Fri, 10 May 2024 19:48:01 -0700 Subject: [PATCH 18/27] [2.14][chore] update release notes (#6705) Updates releases notes. Partially populated with: yarn release_note:generate And gathered the rest. Cleaned up changelog. related to: #6254 Signed-off-by: Kawika Avilla Signed-off-by: yujin-emma --- CHANGELOG.md | 243 +++++++++++------- changelogs/fragments/5652.yml | 2 + changelogs/fragments/6427.yml | 2 - changelogs/fragments/6443.yml | 2 - changelogs/fragments/6477.yml | 2 - changelogs/fragments/6524.yml | 2 - changelogs/fragments/6527.yml | 2 - changelogs/fragments/6544.yml | 2 - changelogs/fragments/6571.yml | 2 - changelogs/fragments/6575.yml | 2 - changelogs/fragments/6584.yml | 2 - changelogs/fragments/6599.yml | 2 - changelogs/fragments/6625.yml | 2 - changelogs/fragments/6636.yml | 2 - changelogs/fragments/6648.yml | 2 - changelogs/fragments/6668.yml | 2 - changelogs/fragments/6669.yml | 2 - changelogs/fragments/6683.yml | 2 - changelogs/fragments/6712.yml | 2 - changelogs/fragments/6722.yml | 2 - ...nsearch-dashboards.release-notes-2.14.0.md | 116 +++++++++ 21 files changed, 265 insertions(+), 132 deletions(-) create mode 100644 changelogs/fragments/5652.yml delete mode 100644 changelogs/fragments/6427.yml delete mode 100644 changelogs/fragments/6443.yml delete mode 100644 changelogs/fragments/6477.yml delete mode 100644 changelogs/fragments/6524.yml delete mode 100644 changelogs/fragments/6527.yml delete mode 100644 changelogs/fragments/6544.yml delete mode 100644 changelogs/fragments/6571.yml delete mode 100644 changelogs/fragments/6575.yml delete mode 100644 changelogs/fragments/6584.yml delete mode 100644 changelogs/fragments/6599.yml delete mode 100644 changelogs/fragments/6625.yml delete mode 100644 changelogs/fragments/6636.yml delete mode 100644 changelogs/fragments/6648.yml delete mode 100644 changelogs/fragments/6668.yml delete mode 100644 changelogs/fragments/6669.yml delete mode 100644 changelogs/fragments/6683.yml delete mode 100644 changelogs/fragments/6712.yml delete mode 100644 changelogs/fragments/6722.yml create mode 100644 release-notes/opensearch-dashboards.release-notes-2.14.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index aa524b4ec4dd..16062b6af7ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,155 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 🛡 Security -- Support dynamic CSP rules to mitigate Clickjacking https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5641 -- [CVE-2020-36604] Employ a patched version of hoek `6.1.3` ([#6148](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6148)) - [CVE-2023-45857] Bump `axios` from `0.27.2` to `1.6.1` ([#5470](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5470)) -- Support dynamic CSP rules to mitigate Clickjacking https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5641 - [WS-2021-0638] Bump mocha from `7.2.0` to `10.1.0` ([#2711](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2711)) + +### 📈 Features/Enhancements + +- [Multiple Datasource] Add multi data source support to Timeline ([#6385](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6385)) +- [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)) + +### 🐛 Bug Fixes + +- [Chore] Update deprecated url methods (url.parse(), url.format()) ([#2910](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2910)) +- Cleanup unused url ([#3847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3847)) + +### 🚞 Infrastructure + +### 📝 Documentation + +### 🛠 Maintenance + +### 🪛 Refactoring + +- Remove unused Sass in `tile_map` plugin ([#4110](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4110)) +- Remove KUI usage in `disabled_lab_visualization` ([#5462](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5462)) + +### 🔩 Tests + +## [2.14.0-2024-05-02](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/tag/2.14.0) + +### 📈 Features/Enhancements + + - Add `opensearchDashboards.futureNavigation` config to control dev tool top right nav button. ([#6712](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6712)) + - Adds `migrations.delete` to delete saved objects by type during a migration ([#6443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6443)) + - Parse query string filters to determine if fields match an index when `ignoreFilterIfFieldNotInIndex` is enabled ([#6126](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6126)) + - [Workspace] Setup workspace skeleton and implement basic CRUD API ([#5075](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5075)) + - [Workspace] Add ACL related functions ([#5084](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5084/)) + - [Workspace] Optional workspaces params in repository ([#5949](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5949)) + - [Workspace] Add delete saved objects by workspace functionality([#6013](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6013)) + - [Workspace] Consume workspace id in saved object client ([#6014](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6014)) + - [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052)) + - [Workspace] Add workspace id in basePath ([#6060](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6060)) + - [Chrome] Introduce registerCollapsibleNavHeader to allow plugins to customize the rendering of nav menu header ([#5244](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5244)) + - [Workspace] Allow making apps available in workspaces using `workspaceAvailability` ([#6427](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6427)) + - [Workspace] Handle data sources and advanced settings as global object. ([#6524](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6524)) + - [Workspace] Make dashboards management available ([#6575](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6575)) + - [Workspace] Add workspace overview page ([#6584](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6584)) + - Improve the perceived performance of Discover when using the default tabular renderer ([#6599](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6599)) + - [Workspace] Hide dashboard overview ([#6625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6625)) + - Optimize scrolling behavior of Discover table ([#6683](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6683)) + - [Discover] Add extension group title to non-index data source groups to indicate log explorer redirection in discover data source selector. ([#5815](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5815)) + - [Multiple Datasource] Create data source menu component able to be mount to nav bar ([#6082](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6082)) + - [Multiple Datasource] Expose filterfn in datasource menu component to allow filter data sources before rendering in navigation bar ([#6113](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6113)) + - [Multiple Datasource] Add component to show single selected data source in read only mode ([#6113](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6125)) + - [Multiple Datasource] Add data source aggregated view to show all compatible data sources or only show used data sources ([#6129](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6129)) + - [Workspace] Register a workspace dropdown menu at the top of left nav bar ([#6150](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6150)) + - [Workspace] Validate if workspace exists when setup inside a workspace ([#6154](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6154)) + - [Multiple Datasource] Add TLS configuration for multiple data sources ([#6171](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6171)) + - [Multiple Datasource] Use data source filter function before rendering ([#6175](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6175)) + - [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179)) + - [Workspace] Add workspace list page ([#6182](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6182)) + - Enable UI Metric Collector to collect UI Metrics and Application Usage ([#6203](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6203)) + - [Multiple Datasource] Add multi selectable data source component ([#6211](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6211)) + - [Multiple Datasource] Add multi data source support to sample vega visualizations ([#6218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6218)) + - [Workspace] Add workspaces column to saved objects page ([#6225](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6225)) + - [Multiple Datasource] Add icon in datasource table page to show the default datasource ([#6231](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6231)) + - [Workspace] Filter left nav menu items according to the current workspace ([#6234](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6234)) + - [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) + - [Multiple DataSource] Codebase maintenance involves updating typos and removing unused imported packages ([#6238](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6238)) + - [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] Remove arrow down icon from data source selectable component ([#6257](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6257)) + - [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 update workspace page ([#6270](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6270)) + - [Workspace] Add API to duplicate saved objects among workspaces ([#6288](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6288)) + - [Multiple Datasource] Enhanced data source selector with default datasource shows as first choice ([#6293](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6293)) + - [Mulitple Datasource] Add multi data source support to TSVB ([#6298](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6298)) + - [Workspace] Add APIs to support plugin state in request ([#6303](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6303)) + - [Multiple Datasource] Fetch data source title for DataSourceView when only id is provided ([#6315](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6315)) + - [Multiple Datasource] 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)) + - Allow the use of `ignoreVersionMismatch` in non-dev configuration ([#6347](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6347)) + - [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)) + - [Multiple Datasource] Get data source label when only id is provided in DataSourceSelectable ([#6358](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6358)) + simplifying client fetch ([#6364](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6364)) + - [Dynamic Configurations] Improve dynamic configurations by adding cache and simplifying client fetch ([#6364](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6364)) + - [Workspace] Support workspace in saved objects client in server side. ([#6365](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6365)) + - [Multiple Datasource] Refactor data source selector component to include placeholder and add tests ([#6372](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6372)) + - [Workspace] Add permission tab to workspace create update page ([#6378](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6378)) + - [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)) + - Replace control characters before logging ([#6402](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6402)) + - [MD] Add dropdown header to data source single selector ([#6431](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6431)) + - [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] Hide datasource and advanced settings menu in dashboard management when in workspace. ([#6455](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6455)) + - [Workspace] Add workspaces filter to saved objects page. ([#6458](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6458)) + - [Multiple Datasource] UI change for datasource view picker to enable selectable([#6497](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6497)) + - [Multiple Datasource] Add popover for empty state and redirect to data source management page([#6514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6514)) + - [Multiple Datasource] Modify selectable picker to remove group label and close popover after selection ([#6515](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6515)) + - [Multiple Datasource] Update empty state font size and footer button size to small ([6549](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6549)) + - Add `rightNavigationButton` component in chrome service for applications to register and add dev tool to top right navigation. ([#6553](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6553)) + - [Multiple Datasource] Extract the button component for datasource picker to avoid duplicate code ([#6559](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6559)) + - [Workspace] Add a workspace client in workspace plugin ([#6094](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6094)) + - [Multiple Datasource] Support multi data source in Region map ([#6654](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6654)) + - [Multiple Datasource] Add empty state component for no connected data source ([#6499](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6499)) + - [MD] Add OpenSearch cluster group label to top of single selectable dropdown ([#6400](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6400)) + +### 🐛 Bug Fixes + + - [Dev Tool] Add additional themed styles to ace overrides ([#5327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5327)) + - [Workspace] Permission check failed with empty workspace for find method ([#6527](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6527)) + - Allow Save in Top Nav Menu to capture filter and query ([#6636](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6636)) + - Fix datasource test connect error ([#6648](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6648)) + - [Workspace] Keep disallowed types when importing with overwrite ([#6668](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6668)) + - [Workspace] Optimization on handling invalid workspace id in workspace_ui_settings wrapper ([#6669](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6669)) + - [Discover] Fix lazy loading of the legacy table from getting stuck ([#6041](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6041)) + - [BUG][Multiple Datasource] Fix obsolete snapshots for test within data source management plugin ([#6185](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6185)) + - [Workspace] Add base path when parse url in http service ([#6233](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6233)) + - [BUG] Fix for checkForFunctionProperty so that order does not matter ([#6248](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6248)) + - [Multiple Datasource] Fix sslConfig for multiple datasource to handle when certificateAuthorities is unset ([#6282](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6282)) + - [BUG][Multiple Datasource]Fix bug in data source aggregated view to change it to depend on displayAllCompatibleDataSources property to show the badge value ([#6291](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6291)) + - [BUG][Multiple Datasource]Read hideLocalCluster setting from yml and set in data source selector and data source menu ([#6361](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6361)) + - [BUG][Multiple Datasource] Refactor read-only component to cover more edge cases ([#6416](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6416)) + - [BUG][Multiple Datasource] Fix style of data source option inside popover for data source selector, selectable, multi select components ([#6438](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6438)) + - [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) + - [VisBuilder] Allow saving and loading filter and query in a saved VisBuilder ([#6460](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6460)) + - [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)) + - [Dynamic Configurations] Fix dynamic config API calls to pass correct input ([#6474](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6474)) + - [BUG][Multiple Datasource] Fix on data source selectable and readonly component are not consistent ([#6545]https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6545) + +### 🚞 Infrastructure + + - Update link-checker and clean up ignore-list ([#6425](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6425)) + +### 🪛 Refactoring + + - Refactor dev tool to use dataSourceManagement.ui API to get DataSourceSelector ([#6477](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6477)) + - Refactor saved object management plugin to use datasourceManagement ui API to get DataSourceSelector ([#6544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6544)) + - discover data selector enhancement and refactoring ([#6571](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6571)) + - [Multiple Datasource] Move data source selectable to its own folder, fix test and a few type errors for data source selectable component ([#6287](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6287)) + - [Multiple Datasource] Remove duplicate data source attribute interface from `data_source_management` ([#6437](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6437)) + +### 🔩 Tests + + - Add functional test cypress workflow improvements and enable the workflow for in-house Dashboards tests ([#6061](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6061)) + +## [2.13.0-2024-03-02](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/tag/2.13.0) + +### 🛡 Security + +- [CVE-2020-36604] Employ a patched version of hoek `6.1.3` ([#6148](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6148)) +- Support dynamic CSP rules to mitigate Clickjacking https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5641 - [CVE-2024-27088] Bump es5-ext from `0.10.59` to `0.10.64` ([#6021](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6021)) - [CVE-2024-28849] Bump follow-redirect from `1.15.4` to `1.15.6` ([#6199](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/6199)) @@ -29,98 +173,30 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Improved error handling for the search API when a null value is passed for the dataSourceId ([#5882](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5882)) - [Multiple Datasource] Hide/Show authentication method in multi data source plugin based on configuration ([#5916](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5916)) - [Dynamic Configurations] Add support for dynamic application configurations ([#5855](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5855)) -- [Workspace] Setup workspace skeleton and implement basic CRUD API ([#5075](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5075)) -- [Workspace] Add ACL related functions ([#5084](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5084/)) -- [Workspace] Optional workspaces params in repository ([#5949](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5949)) - [Multiple Datasource] Refactoring create and edit form to use authentication registry ([#6002](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6002)) - [Multiple Datasource] Handles auth methods from auth registry in DataSource SavedObjects Client Wrapper ([#6062](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6062)) - [Multiple Datasource] Expose a few properties for customize the appearance of the data source selector component ([#6057](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6057)) -- [Multiple Datasource] Create data source menu component able to be mount to nav bar ([#6082](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6082)) - [Multiple Datasource] Handle form values(request payload) if the selected type is available in the authentication registry ([#6049](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6049)) - [Multiple Datasource] Adds a session token to AWS credentials ([#6103](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6103)) - [Multiple Datasource] Add Vega support to MDS by specifying a data source name in the Vega spec ([#5975](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5975)) - [Multiple Datasource] Test connection schema validation for registered auth types ([#6109](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6109)) - [Multiple DataSource] DataSource creation and edition page improvement to better support registered auth types ([#6122](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6122)) -- [Multiple DataSource] Codebase maintenance involves updating typos and removing unused imported packages ([#6238](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6238)) -- [Workspace] Consume workspace id in saved object client ([#6014](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6014)) - [Multiple Datasource] Export DataSourcePluginRequestContext at top level for plugins to use ([#6108](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6108)) -- [Multiple Datasource] Expose filterfn in datasource menu component to allow filter data sources before rendering in navigation bar ([#6113](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6113)) - [Multiple Datasource] Improves connection pooling support for AWSSigV4 clients in data sources ([#6135](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6135)) -- [Workspace] Add delete saved objects by workspace functionality([#6013](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6013)) -- [Workspace] Add a workspace client in workspace plugin ([#6094](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6094)) -- [Multiple Datasource] Add component to show single selected data source in read only mode ([#6125](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6125)) -- [Multiple Datasource] Add data source aggregated view to show all compatible data sources or only show used data sources ([#6129](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6129)) - [Multiple Datasource] Add datasource version number to newly created data source object([#6178](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6178)) -- [Workspace] Add workspace id in basePath ([#6060](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6060)) - Implement new home page ([#6065](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6065)) - Add sidecar service ([#5920](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5920)) -- Allow the use of `ignoreVersionMismatch` in non-dev configuration ([#6347](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6347)) -- [Multiple Datasource] Use data source filter function before rendering ([#6175](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6175)) -- [Chrome] Introduce registerCollapsibleNavHeader to allow plugins to customize the rendering of nav menu header ([#5244](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5244)) - [Dynamic Configurations] Pass request headers when making application config calls ([#6164](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6164)) - [Discover] Options button to configure legacy mode and remove the top navigation option ([#6170](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6170)) - [Multiple Datasource] Add default functionality for customer to choose default datasource ([#6058](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/6058)) -- [Multiple Datasource] Remove arrow down icon from data source selectable component ([#6257](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6257)) - [Multiple Datasource] Add import support for Vega when specifying a datasource ([#6123](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6123)) -- [Workspace] Validate if workspace exists when setup inside a workspace ([#6154](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6154)) -- [Workspace] Register a workspace dropdown menu at the top of left nav bar ([#6150](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6150)) -- [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)) -- [Workspace] Add update workspace page ([#6270](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6270)) -- [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) -- [Workspace] Add workspace list page ([#6182](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6182)) -- [Workspace] Add API to duplicate saved objects among workspaces ([#6288](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6288)) -- [Workspace] Add workspaces column to saved objects page ([#6225](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6225)) -- [Multiple Datasource] Enhanced data source selector with default datasource shows as first choice ([#6293](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6293)) -- [Multiple Datasource] Add multi data source support to sample vega visualizations ([#6218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6218)) -- [Multiple Datasource] Fetch data source title for DataSourceView when only id is provided ([#6315](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6315) -- [Multiple Datasource] Get data source label when only id is provided in DataSourceSelectable ([#6358](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6358) -- [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052)) -- [Multiple Datasource] Add default icon for selectable component and make sure the default datasource shows automatically ([#6327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6327)) -- [Multiple Datasource] Pass selected data sources to plugin consumers when the multi-select component initially loads ([#6333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6333)) -- [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)) -- [Multiple Datasource] Add empty state component for no connected data source ([#6499](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6499)) -- [Multiple Datasource] Add popover for empty state and redirect to data source management page([#6514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6514)) -- [Multiple Datasource] Update empty state font size and footer button size to small ([6549](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6549)) -- [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)) -- [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)) -- [Multiple Datasource] UI change for datasource view picker to enable selectable([#6497](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6497)) -- [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)) -- [Workspace] Add permission tab to workspace create update page ([#6378](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6378)) -- [Workspace] Hide datasource and advanced settings menu in dashboard management when in workspace. ([#6455](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6455)) -- [Multiple Datasource] Modify selectable picker to remove group label and close popover after selection ([#6515](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6515)) -- [Multiple Datasource] Extract the button component for datasource picker to avoid duplicate code ([#6559](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6559)) -- [Workspace] Add workspaces filter to saved objects page. ([#6458](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6458)) -- [Multiple Datasource] Support multi data source in Region map ([#6654](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6654)) -- Add `rightNavigationButton` component in chrome service for applications to register and add dev tool to top right navigation. ([#6553](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6553)) -- Enable UI Metric Collector to collect UI Metrics and Application Usage ([#6203](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6203)) -- Add `opensearchDashboards.futureNavigation` config to control dev tool top right nav button. ([#6712](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6712)) ### 🐛 Bug Fixes -- [Chore] Update deprecated url methods (url.parse(), url.format()) ([#2910](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2910)) -- Cleanup unused url ([#3847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3847)) -- [BUG][Discover] Allow saved sort from search embeddable to load in Dashboard ([#5934](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5934)) - [BUG][Discover] Add key to index pattern options for support deplicate index pattern names([#5946](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5946)) - [Discover] Fix table cell content overflowing in Safari ([#5948](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5948)) - [BUG][MD]Fix schema for test connection to separate validation based on auth type ([#5997](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5997)) - [Discover] Enable 'Back to Top' Feature in Discover for scrolling to top ([#6008](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6008)) -- [Discover] Fix lazy loading of the legacy table from getting stuck ([#6041](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6041)) - [BUG][Discover] Allow saved sort from search embeddable to load in Dashboard ([#5934](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5934)) - [osd/std] Add additional recovery from false-positives in handling of long numerals ([#5956](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5956), [#6245](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6245)) - [osd/std] Add fallback mechanism when recovery from false-positives in handling of long numerals fails ([#6253](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6253)) @@ -128,21 +204,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [BUG][Multiple Datasource] Add a migration function for datasource to add migrationVersion field ([#6025](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6025)) - [BUG][MD]Expose picker using function in data source management plugin setup([#6030](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6030)) - [BUG][Multiple Datasource] Fix data source filter bug and add tests ([#6152](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6152)) -- [BUG][Multiple Datasource] Fix obsolete snapshots for test within data source management plugin ([#6185](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6185)) -- [Workspace] Add base path when parse url in http service ([#6233](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6233)) -- [Multiple Datasource] Fix sslConfig for multiple datasource to handle when certificateAuthorities is unset ([#6282](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6282)) -- [BUG][Multiple Datasource]Fix bug in data source aggregated view to change it to depend on displayAllCompatibleDataSources property to show the badge value ([#6291](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6291)) -- [BUG][Multiple Datasource] Fix style of data source option inside popover for data source selector, selectable, multi select components ([#6438](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6438)) -- [BUG][Multiple Datasource]Read hideLocalCluster setting from yml and set in data source selector and data source menu ([#6361](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6361)) -- [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] Fix on data source selectable and readonly component are not consistent ([#6545]https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6545) -- [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)) -- [Dev Tool] Add additional themed styles to ace overrides ([#5327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5327)) -- [VisBuilder] Allow saving and loading filter and query in a saved VisBuilder ([#6460](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6460)) ### 🚞 Infrastructure @@ -150,7 +212,6 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Re-enable CI workflows for feature branches ([#2908](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2908)) - [Tests] Add Github workflow for Test Orchestrator in FT Repo to run cypress tests within Dashboards repo ([#5725](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5725)) - Upgrade yarn version to be compatible with @opensearch-project/opensearch ([#3443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3443)) -- Update link-checker and clean up ignore-list ([#6425](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6425)) ### 📝 Documentation @@ -172,16 +233,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Move @kristenTian to emeritus maintainer ([#6136](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6136)) - Add @xinruiba as a maintainer ([#6217](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6217)) -### 🪛 Refactoring - -- Remove unused Sass in `tile_map` plugin ([#4110](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4110)) -- [Multiple Datasource] Move data source selectable to its own folder, fix test and a few type errors for data source selectable component ([#6287](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6287)) -- Remove KUI usage in `disabled_lab_visualization` ([#5462](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5462)) -- [Multiple Datasource] Remove duplicate data source attribute interface from `data_source_management` ([#6437](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6437)) - ### 🔩 Tests -- Add functional test cypress workflow improvements and enable the workflow for in-house Dashboards tests ([#6061](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6061)) - Rename cypress config file to its version supported convention ([#6137](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6137)) ## [2.12.0 - 2024-02-20](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/tag/2.12.0) @@ -211,13 +264,11 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Discover] Added customizable pagination options based on Discover UI settings [#5610](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5610) - [PM] Enhance single version requirements imposed during bootstrapping ([#5675](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5675)) - [Custom Branding] Relative URL should be allowed for logos ([#5572](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5572)) -- [Theme] Make theme and dark mode settings user/device specific (in local storage), with opt-out ([#5652](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5652)) - Revert to legacy discover table and add toggle to new discover table ([#5789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5789)) - [Discover] Add collapsible and resizeable sidebar ([#5789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5789)) - [Discover] Enhanced the data source selector with added sorting functionality ([#5719](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5719)) - [Multiple Datasource] Add datasource picker component and use it in devtools and tutorial page when multiple datasource is enabled ([#5756](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5756)) - [Multiple Datasource] Add datasource picker to import saved object flyout when multiple data source is enabled ([#5781](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5781)) -- [Discover] Add extension group title to non-index data source groups to indicate log explorer redirection in discover data source selector. ([#5815](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5815)) ### 🐛 Bug Fixes diff --git a/changelogs/fragments/5652.yml b/changelogs/fragments/5652.yml new file mode 100644 index 000000000000..fe6ed9dc1da3 --- /dev/null +++ b/changelogs/fragments/5652.yml @@ -0,0 +1,2 @@ +feat: +- Make theme and dark mode settings user/device specific (in local storage), with opt-out ([#5652](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5652)) \ No newline at end of file diff --git a/changelogs/fragments/6427.yml b/changelogs/fragments/6427.yml deleted file mode 100644 index 74066816aad3..000000000000 --- a/changelogs/fragments/6427.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- [Workspace] Allow making apps available in workspaces using `workspaceAvailability` ([#6427](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6427)) \ No newline at end of file diff --git a/changelogs/fragments/6443.yml b/changelogs/fragments/6443.yml deleted file mode 100644 index 2de7191e0d09..000000000000 --- a/changelogs/fragments/6443.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- Adds `migrations.delete` to delete saved objects by type during a migration ([#6443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6443)) \ No newline at end of file diff --git a/changelogs/fragments/6477.yml b/changelogs/fragments/6477.yml deleted file mode 100644 index fe9c055906f4..000000000000 --- a/changelogs/fragments/6477.yml +++ /dev/null @@ -1,2 +0,0 @@ -refactor: -- Refactor dev tool to use dataSourceManagement.ui API to get DataSourceSelector ([#6477](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6477)) \ No newline at end of file diff --git a/changelogs/fragments/6524.yml b/changelogs/fragments/6524.yml deleted file mode 100644 index 1c7c99bb0145..000000000000 --- a/changelogs/fragments/6524.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- [Workspace] Handle data sources and advanced settings as global object. ([#6524](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6524)) \ No newline at end of file diff --git a/changelogs/fragments/6527.yml b/changelogs/fragments/6527.yml deleted file mode 100644 index 0ae1aef9cac1..000000000000 --- a/changelogs/fragments/6527.yml +++ /dev/null @@ -1,2 +0,0 @@ -fix: -- Permission check failed with empty workspace for find method ([#6527](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6527)) \ No newline at end of file diff --git a/changelogs/fragments/6544.yml b/changelogs/fragments/6544.yml deleted file mode 100644 index 53d6283a6d47..000000000000 --- a/changelogs/fragments/6544.yml +++ /dev/null @@ -1,2 +0,0 @@ -refactor: -- Refactor saved object management plugin to use datasourceManagement ui API to get DataSourceSelector ([#6544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6544)) \ No newline at end of file diff --git a/changelogs/fragments/6571.yml b/changelogs/fragments/6571.yml deleted file mode 100644 index a6e341fc15e4..000000000000 --- a/changelogs/fragments/6571.yml +++ /dev/null @@ -1,2 +0,0 @@ -refactor: -- discover data selector enhancement and refactoring ([#6571](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6571)) \ No newline at end of file diff --git a/changelogs/fragments/6575.yml b/changelogs/fragments/6575.yml deleted file mode 100644 index 964c04770a83..000000000000 --- a/changelogs/fragments/6575.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- [Workspace] Make dashboards management available ([#6575](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6575)) \ No newline at end of file diff --git a/changelogs/fragments/6584.yml b/changelogs/fragments/6584.yml deleted file mode 100644 index 69b9d2471271..000000000000 --- a/changelogs/fragments/6584.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- [Workspace] Add workspace overview page ([#6584](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6584)) \ No newline at end of file diff --git a/changelogs/fragments/6599.yml b/changelogs/fragments/6599.yml deleted file mode 100644 index 0cdaf731e500..000000000000 --- a/changelogs/fragments/6599.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- Improve the perceived performance of Discover when using the default tabular renderer ([#6599](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6599)) diff --git a/changelogs/fragments/6625.yml b/changelogs/fragments/6625.yml deleted file mode 100644 index cd5e6e8ead0a..000000000000 --- a/changelogs/fragments/6625.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- [Workspace] Hide dashboard overview ([#6625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6625)) \ No newline at end of file diff --git a/changelogs/fragments/6636.yml b/changelogs/fragments/6636.yml deleted file mode 100644 index 2434dd458b9f..000000000000 --- a/changelogs/fragments/6636.yml +++ /dev/null @@ -1,2 +0,0 @@ -fix: -- [BUG] Allow Save in Top Nav Menu to capture filter and query ([#6636](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6636)) \ No newline at end of file diff --git a/changelogs/fragments/6648.yml b/changelogs/fragments/6648.yml deleted file mode 100644 index e3d207dedbcd..000000000000 --- a/changelogs/fragments/6648.yml +++ /dev/null @@ -1,2 +0,0 @@ -fix: -- Fix datasource test connect error ([#6648](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6648)) \ No newline at end of file diff --git a/changelogs/fragments/6668.yml b/changelogs/fragments/6668.yml deleted file mode 100644 index 239ee0257197..000000000000 --- a/changelogs/fragments/6668.yml +++ /dev/null @@ -1,2 +0,0 @@ -fix: -- Keep disallowed types when importing with overwrite ([#6668](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6668)) \ No newline at end of file diff --git a/changelogs/fragments/6669.yml b/changelogs/fragments/6669.yml deleted file mode 100644 index 1e68c7b5477a..000000000000 --- a/changelogs/fragments/6669.yml +++ /dev/null @@ -1,2 +0,0 @@ -fix: -- Optimization on handling invalid workspace id in workspace_ui_settings wrapper ([#6669](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6669)) \ No newline at end of file diff --git a/changelogs/fragments/6683.yml b/changelogs/fragments/6683.yml deleted file mode 100644 index 1a065c9c0c51..000000000000 --- a/changelogs/fragments/6683.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- Optimize scrolling behavior of Discover table ([#6683](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6683)) diff --git a/changelogs/fragments/6712.yml b/changelogs/fragments/6712.yml deleted file mode 100644 index 36e5ebc65001..000000000000 --- a/changelogs/fragments/6712.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- Add `opensearchDashboards.futureNavigation` config to control dev tool top right nav button. ([#6712](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6712)) \ No newline at end of file diff --git a/changelogs/fragments/6722.yml b/changelogs/fragments/6722.yml deleted file mode 100644 index 6e4c4511ac0a..000000000000 --- a/changelogs/fragments/6722.yml +++ /dev/null @@ -1,2 +0,0 @@ -fix: -- Test failures related to #6443 ([#6722](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6722)) \ No newline at end of file diff --git a/release-notes/opensearch-dashboards.release-notes-2.14.0.md b/release-notes/opensearch-dashboards.release-notes-2.14.0.md new file mode 100644 index 000000000000..37dcf89f3eb9 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.14.0.md @@ -0,0 +1,116 @@ +## Version 2.14.0 Release Notes + +### 📈 Features/Enhancements + + - Add `opensearchDashboards.futureNavigation` config to control dev tool top right nav button. ([#6712](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6712)) + - Adds `migrations.delete` to delete saved objects by type during a migration ([#6443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6443)) + - Parse query string filters to determine if fields match an index when `ignoreFilterIfFieldNotInIndex` is enabled ([#6126](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6126)) + - [Workspace] Setup workspace skeleton and implement basic CRUD API ([#5075](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5075)) + - [Workspace] Add ACL related functions ([#5084](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5084/)) + - [Workspace] Optional workspaces params in repository ([#5949](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5949)) + - [Workspace] Add delete saved objects by workspace functionality([#6013](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6013)) + - [Workspace] Consume workspace id in saved object client ([#6014](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6014)) + - [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052)) + - [Workspace] Add workspace id in basePath ([#6060](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6060)) + - [Chrome] Introduce registerCollapsibleNavHeader to allow plugins to customize the rendering of nav menu header ([#5244](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5244)) + - [Workspace] Allow making apps available in workspaces using `workspaceAvailability` ([#6427](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6427)) + - [Workspace] Handle data sources and advanced settings as global object. ([#6524](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6524)) + - [Workspace] Make dashboards management available ([#6575](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6575)) + - [Workspace] Add workspace overview page ([#6584](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6584)) + - Improve the perceived performance of Discover when using the default tabular renderer ([#6599](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6599)) + - [Workspace] Hide dashboard overview ([#6625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6625)) + - Optimize scrolling behavior of Discover table ([#6683](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6683)) + - [Discover] Add extension group title to non-index data source groups to indicate log explorer redirection in discover data source selector. ([#5815](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5815)) + - [Multiple Datasource] Create data source menu component able to be mount to nav bar ([#6082](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6082)) + - [Multiple Datasource] Expose filterfn in datasource menu component to allow filter data sources before rendering in navigation bar ([#6113](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6113)) + - [Multiple Datasource] Add component to show single selected data source in read only mode ([#6113](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6125)) + - [Multiple Datasource] Add data source aggregated view to show all compatible data sources or only show used data sources ([#6129](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6129)) + - [Workspace] Register a workspace dropdown menu at the top of left nav bar ([#6150](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6150)) + - [Workspace] Validate if workspace exists when setup inside a workspace ([#6154](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6154)) + - [Multiple Datasource] Add TLS configuration for multiple data sources ([#6171](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6171)) + - [Multiple Datasource] Use data source filter function before rendering ([#6175](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6175)) + - [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179)) + - [Workspace] Add workspace list page ([#6182](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6182)) + - Enable UI Metric Collector to collect UI Metrics and Application Usage ([#6203](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6203)) + - [Multiple Datasource] Add multi selectable data source component ([#6211](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6211)) + - [Multiple Datasource] Add multi data source support to sample vega visualizations ([#6218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6218)) + - [Workspace] Add workspaces column to saved objects page ([#6225](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6225)) + - [Multiple Datasource] Add icon in datasource table page to show the default datasource ([#6231](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6231)) + - [Workspace] Filter left nav menu items according to the current workspace ([#6234](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6234)) + - [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) + - [Multiple DataSource] Codebase maintenance involves updating typos and removing unused imported packages ([#6238](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6238)) + - [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] Remove arrow down icon from data source selectable component ([#6257](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6257)) + - [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 update workspace page ([#6270](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6270)) + - [Workspace] Add API to duplicate saved objects among workspaces ([#6288](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6288)) + - [Multiple Datasource] Enhanced data source selector with default datasource shows as first choice ([#6293](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6293)) + - [Mulitple Datasource] Add multi data source support to TSVB ([#6298](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6298)) + - [Workspace] Add APIs to support plugin state in request ([#6303](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6303)) + - [Multiple Datasource] Fetch data source title for DataSourceView when only id is provided ([#6315](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6315)) + - [Multiple Datasource] 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)) + - Allow the use of `ignoreVersionMismatch` in non-dev configuration ([#6347](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6347)) + - [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)) + - [Multiple Datasource] Get data source label when only id is provided in DataSourceSelectable ([#6358](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6358)) + simplifying client fetch ([#6364](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6364)) + - [Dynamic Configurations] Improve dynamic configurations by adding cache and simplifying client fetch ([#6364](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6364)) + - [Workspace] Support workspace in saved objects client in server side. ([#6365](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6365)) + - [Multiple Datasource] Refactor data source selector component to include placeholder and add tests ([#6372](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6372)) + - [Workspace] Add permission tab to workspace create update page ([#6378](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6378)) + - [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)) + - Replace control characters before logging ([#6402](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6402)) + - [MD] Add dropdown header to data source single selector ([#6431](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6431)) + - [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] Hide datasource and advanced settings menu in dashboard management when in workspace. ([#6455](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6455)) + - [Workspace] Add workspaces filter to saved objects page. ([#6458](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6458)) + - [Multiple Datasource] UI change for datasource view picker to enable selectable([#6497](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6497)) + - [Multiple Datasource] Add popover for empty state and redirect to data source management page([#6514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6514)) + - [Multiple Datasource] Modify selectable picker to remove group label and close popover after selection ([#6515](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6515)) + - [Multiple Datasource] Update empty state font size and footer button size to small ([6549](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6549)) + - Add `rightNavigationButton` component in chrome service for applications to register and add dev tool to top right navigation. ([#6553](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6553)) + - [Multiple Datasource] Extract the button component for datasource picker to avoid duplicate code ([#6559](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6559)) + - [Workspace] Add a workspace client in workspace plugin ([#6094](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6094)) + - [Multiple Datasource] Support multi data source in Region map ([#6654](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6654)) + - [Multiple Datasource] Add empty state component for no connected data source ([#6499](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6499)) + - [MD] Add OpenSearch cluster group label to top of single selectable dropdown ([#6400](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6400)) + +### 🐛 Bug Fixes + + - [Dev Tool] Add additional themed styles to ace overrides ([#5327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5327)) + - [Workspace] Permission check failed with empty workspace for find method ([#6527](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6527)) + - Allow Save in Top Nav Menu to capture filter and query ([#6636](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6636)) + - Fix datasource test connect error ([#6648](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6648)) + - [Workspace] Keep disallowed types when importing with overwrite ([#6668](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6668)) + - [Workspace] Optimization on handling invalid workspace id in workspace_ui_settings wrapper ([#6669](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6669)) + - [Discover] Fix lazy loading of the legacy table from getting stuck ([#6041](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6041)) + - [BUG][Multiple Datasource] Fix obsolete snapshots for test within data source management plugin ([#6185](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6185)) + - [Workspace] Add base path when parse url in http service ([#6233](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6233)) + - [BUG] Fix for checkForFunctionProperty so that order does not matter ([#6248](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6248)) + - [Multiple Datasource] Fix sslConfig for multiple datasource to handle when certificateAuthorities is unset ([#6282](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6282)) + - [BUG][Multiple Datasource]Fix bug in data source aggregated view to change it to depend on displayAllCompatibleDataSources property to show the badge value ([#6291](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6291)) + - [BUG][Multiple Datasource]Read hideLocalCluster setting from yml and set in data source selector and data source menu ([#6361](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6361)) + - [BUG][Multiple Datasource] Refactor read-only component to cover more edge cases ([#6416](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6416)) + - [BUG][Multiple Datasource] Fix style of data source option inside popover for data source selector, selectable, multi select components ([#6438](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6438)) + - [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) + - [VisBuilder] Allow saving and loading filter and query in a saved VisBuilder ([#6460](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6460)) + - [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)) + - [Dynamic Configurations] Fix dynamic config API calls to pass correct input ([#6474](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6474)) + - [BUG][Multiple Datasource] Fix on data source selectable and readonly component are not consistent ([#6545]https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6545) + +### 🚞 Infrastructure + + - Update link-checker and clean up ignore-list ([#6425](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6425)) + +### 🪛 Refactoring + + - Refactor dev tool to use dataSourceManagement.ui API to get DataSourceSelector ([#6477](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6477)) + - Refactor saved object management plugin to use datasourceManagement ui API to get DataSourceSelector ([#6544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6544)) + - discover data selector enhancement and refactoring ([#6571](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6571)) + - [Multiple Datasource] Move data source selectable to its own folder, fix test and a few type errors for data source selectable component ([#6287](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6287)) + - [Multiple Datasource] Remove duplicate data source attribute interface from `data_source_management` ([#6437](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6437)) + +### 🔩 Tests + + - Add functional test cypress workflow improvements and enable the workflow for in-house Dashboards tests ([#6061](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6061)) From 18d1b1da365fe13506aa122b97e6fe3e077a61af Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Mon, 13 May 2024 15:30:38 -0700 Subject: [PATCH 19/27] add http://www.site.com to lycheeignore (#6771) * add http://www.site.com to lycheeignore Signed-off-by: yujin-emma * Changeset file for PR #6771 created/updated * Update .lycheeignore Co-authored-by: Miki Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> --------- Signed-off-by: yujin-emma Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Miki Signed-off-by: yujin-emma --- .lycheeignore | 1 + changelogs/fragments/6771.yml | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelogs/fragments/6771.yml diff --git a/.lycheeignore b/.lycheeignore index 6d000f2d65ae..89b3c520d87d 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -88,3 +88,4 @@ https://unpkg.com/@elastic/ https://codeload.github.com/ https://www.quandl.com/api/v1/datasets/ https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi +site.com diff --git a/changelogs/fragments/6771.yml b/changelogs/fragments/6771.yml new file mode 100644 index 000000000000..454dd0d17565 --- /dev/null +++ b/changelogs/fragments/6771.yml @@ -0,0 +1,2 @@ +fix: +- Lint checker failure fix ([#6771](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6771)) \ No newline at end of file From 1f68fea4f258489967ea155df5f118d9b139db79 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Tue, 14 May 2024 13:20:34 +0800 Subject: [PATCH 20/27] [Workspace] Fix: Show a error toast when workspace read only user delete saved objects (#6756) * fix: show a error toast when workspace read only user delete saved objects Signed-off-by: yubonluo * Changeset file for PR #6756 created/updated * Changeset file for PR #6756 created/updated * optimize the code Signed-off-by: yubonluo * Display the delete modal after failing to delete. Signed-off-by: yubonluo * Add some unit tests Signed-off-by: yubonluo * Add some state assertions Signed-off-by: yubonluo --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6756.yml | 2 + .../saved_objects_table.test.tsx.snap | 140 ++++++++++++++++++ .../saved_objects_table.test.tsx | 50 +++++++ .../objects_table/saved_objects_table.tsx | 52 ++++--- 4 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 changelogs/fragments/6756.yml diff --git a/changelogs/fragments/6756.yml b/changelogs/fragments/6756.yml new file mode 100644 index 000000000000..9cad7bffc99d --- /dev/null +++ b/changelogs/fragments/6756.yml @@ -0,0 +1,2 @@ +fix: +- Show error toast when fail to delete saved objects ([#6756](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6756)) \ No newline at end of file diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index fdff7b9d913b..6c56eed79fe4 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -74,6 +74,146 @@ exports[`SavedObjectsTable delete should show a confirm modal 1`] = ` `; +exports[`SavedObjectsTable delete should show error toast when failing to delete saved objects 1`] = ` + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } +> +

+ +

+ +
+`; + +exports[`SavedObjectsTable delete should show error toast when failing to delete saved objects 2`] = ` + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } +> +

+ +

+ +
+`; + exports[`SavedObjectsTable export should allow the user to choose when exporting all 1`] = ` { // Set some as selected component.instance().onSelectionChanged(mockSelectedSavedObjects); + await component.instance().onDelete(); + component.update(); + expect(component.state('isShowingDeleteConfirmModal')).toBe(true); await component.instance().delete(); @@ -612,6 +615,53 @@ describe('SavedObjectsTable', () => { { force: true } ); expect(component.state('selectedSavedObjects').length).toBe(0); + expect(notifications.toasts.addDanger).not.toHaveBeenCalled(); + expect(component.state('isDeleting')).toBe(false); + expect(component.state('isShowingDeleteConfirmModal')).toBe(false); + }); + + it('should show error toast when failing to delete saved objects', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + ] as SavedObjectWithMetadata[]; + + const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({ + id: obj.id, + type: obj.type, + source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + delete: jest.fn().mockImplementation(() => { + throw new Error('Unable to delete saved objects'); + }), + }; + + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + await component.instance().onDelete(); + component.update(); + expect(component.state('isShowingDeleteConfirmModal')).toBe(true); + expect(component.find('EuiConfirmModal')).toMatchSnapshot(); + + await component.instance().delete(); + component.update(); + expect(notifications.toasts.addDanger).toHaveBeenCalled(); + // If user fail to delete the saved objects, the delete modal will continue to display + expect(component.state('isShowingDeleteConfirmModal')).toBe(true); + expect(component.find('EuiConfirmModal')).toMatchSnapshot(); + expect(component.state('isDeleting')).toBe(false); }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 56e7950efeea..04d7a46d24d2 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -589,7 +589,7 @@ export class SavedObjectsTable extends Component { - const { savedObjectsClient } = this.props; + const { savedObjectsClient, notifications } = this.props; const { selectedSavedObjects, isDeleting } = this.state; if (isDeleting) { @@ -599,30 +599,38 @@ export class SavedObjectsTable extends Component object.type === 'index-pattern'); - if (indexPatterns.length) { - await this.props.indexPatterns.clearCache(); - } - - const objects = await savedObjectsClient.bulkGet(selectedSavedObjects); - const deletes = objects.savedObjects.map((object) => - savedObjectsClient.delete(object.type, object.id, { force: true }) - ); - await Promise.all(deletes); - // Unset this - this.setState({ - selectedSavedObjects: [], - }); + try { + if (indexPatterns.length) { + await this.props.indexPatterns.clearCache(); + } + const objects = await savedObjectsClient.bulkGet(selectedSavedObjects); + const deletes = objects.savedObjects.map((object) => + savedObjectsClient.delete(object.type, object.id, { force: true }) + ); + await Promise.all(deletes); + // Unset this + this.setState({ + selectedSavedObjects: [], + }); + // Fetching all data + await this.fetchSavedObjects(); + await this.fetchCounts(); - // Fetching all data - await this.fetchSavedObjects(); - await this.fetchCounts(); + // Allow the user to interact with the table once the saved objects have been re-fetched. + // If the user fails to delete the saved objects, the delete modal will continue to display. + this.setState({ isShowingDeleteConfirmModal: false }); + } catch (error) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.unableDeleteSavedObjectsNotificationMessage', + { defaultMessage: 'Unable to delete saved objects' } + ), + text: `${error}`, + }); + } - // Allow the user to interact with the table once the saved objects have been re-fetched. - this.setState({ - isShowingDeleteConfirmModal: false, - isDeleting: false, - }); + this.setState({ isDeleting: false }); }; getRelationships = async (type: string, id: string) => { From 297eaa5115ecc208c81b4c1b3f705af6fabb16f5 Mon Sep 17 00:00:00 2001 From: Varun Lodaya Date: Tue, 14 May 2024 19:36:06 +0530 Subject: [PATCH 21/27] Updating security reachout email (#6778) Signed-off-by: varun-lodaya Signed-off-by: yujin-emma --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index f06efd5cf4ed..22c3b51933e3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ ## Reporting a Vulnerability -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. Please do **not** create a public GitHub issue. +If you discover a potential security issue in this project we ask that you notify OpenSearch Security directly via email to security@opensearch.org. Please do **not** create a public GitHub issue. ## Fixing a Vulnerability From 57ca1ab8583e6ed1d6558efa3dea20b61af51385 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Tue, 14 May 2024 10:38:40 -0700 Subject: [PATCH 22/27] add @zhyuanqi as a maintainer (#6788) * add @zhyuanqi as a maintainer --------- Signed-off-by: Lu Yu Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- .github/CODEOWNERS | 2 +- MAINTAINERS.md | 1 + changelogs/fragments/6788.yml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/6788.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bc7e7b27c7d5..88e62892d9a9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @ananzh @kavilla @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @BSFishy @curq @bandinib-amzn @SuZhou-Joe @ruanyl @BionIT @xinruiba +* @ananzh @kavilla @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @BSFishy @curq @bandinib-amzn @SuZhou-Joe @ruanyl @BionIT @xinruiba @zhyuanqi diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 4f9791b3949f..d6155efcd837 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -24,6 +24,7 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Yulong Ruan | [ruanyl](https://github.com/ruanyl) | Amazon | | Lu Yu | [BionIT](https://github.com/BionIT) | Amazon | | Xinrui Bai | [xinruiba](https://github.com/xinruiba) | Amazon | +| Ella Zhu | [zhyuanqi](https://github.com/zhyuanqi) | Amazon | ## Emeritus diff --git a/changelogs/fragments/6788.yml b/changelogs/fragments/6788.yml new file mode 100644 index 000000000000..a032e911c446 --- /dev/null +++ b/changelogs/fragments/6788.yml @@ -0,0 +1,2 @@ +doc: +- Add zhyuanqi as maintainer ([#6788](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6788)) \ No newline at end of file From 9560ca061856fe3f442b7fb9c03dcfee1ced560d Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 14 May 2024 18:52:27 -0700 Subject: [PATCH 23/27] [OE] Adds dev doc script to precommit hook (#6585) * Adds doc generation to pre commit hook Signed-off-by: Ashwin P Chandran * Add check hook to pre commit Signed-off-by: Ashwin P Chandran * Changeset file for PR #6585 created/updated * Update error message Signed-off-by: Ashwin P Chandran * Improve error message Co-authored-by: Miki Signed-off-by: Ashwin P Chandran * fixes lint issue Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Ashwin P Chandran Signed-off-by: Ashwin P Chandran Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Miki Signed-off-by: yujin-emma --- changelogs/fragments/6585.yml | 2 + scripts/precommit_hook.js | 1 + src/dev/precommit_hook/check_dev_docs.js | 18 ++++++++ .../precommit_hook/get_files_for_commit.js | 42 +++++++++++++------ src/dev/precommit_hook/index.js | 3 +- src/dev/run_precommit_hook.js | 15 ++++++- 6 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 changelogs/fragments/6585.yml create mode 100644 src/dev/precommit_hook/check_dev_docs.js diff --git a/changelogs/fragments/6585.yml b/changelogs/fragments/6585.yml new file mode 100644 index 000000000000..311fa8da3abc --- /dev/null +++ b/changelogs/fragments/6585.yml @@ -0,0 +1,2 @@ +chore: +- Adds a git pre commit hook to ensure that developer docs are always updated ([#6585](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6585)) \ No newline at end of file diff --git a/scripts/precommit_hook.js b/scripts/precommit_hook.js index 1d5485c64905..eb5347071386 100644 --- a/scripts/precommit_hook.js +++ b/scripts/precommit_hook.js @@ -29,4 +29,5 @@ */ require('@osd/optimizer').registerNodeAutoTranspilation(); +require('./generate_docs_sidebar'); require('../src/dev/run_precommit_hook'); diff --git a/src/dev/precommit_hook/check_dev_docs.js b/src/dev/precommit_hook/check_dev_docs.js new file mode 100644 index 000000000000..3a134dffb2b3 --- /dev/null +++ b/src/dev/precommit_hook/check_dev_docs.js @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const SIDEBAR_PATH = 'docs/_sidebar.md'; + +export async function checkDevDocs(log, files) { + files.map((file) => { + const path = file.getRelativePath(); + + if (path === SIDEBAR_PATH) { + throw Error( + `The ${SIDEBAR_PATH} file of the developer docs has been modified but is not ready to be committed. This can be done by performing "git add ${SIDEBAR_PATH}" and committing the changes.` + ); + } + }); +} diff --git a/src/dev/precommit_hook/get_files_for_commit.js b/src/dev/precommit_hook/get_files_for_commit.js index b0a18f650f70..7691df3c0fa7 100644 --- a/src/dev/precommit_hook/get_files_for_commit.js +++ b/src/dev/precommit_hook/get_files_for_commit.js @@ -34,20 +34,9 @@ import { fromNode as fcb } from 'bluebird'; import { REPO_ROOT } from '@osd/utils'; import { File } from '../file'; -/** - * Get the files that are staged for commit (excluding deleted files) - * as `File` objects that are aware of their commit status. - * - * @param {String} repoPath - * @return {Promise>} - */ -export async function getFilesForCommit() { - const simpleGit = new SimpleGit(REPO_ROOT); - - const output = await fcb((cb) => simpleGit.diff(['--name-status', '--cached'], cb)); - +function getFileList(diffText) { return ( - output + diffText .split('\n') // Ignore blank lines .filter((line) => line.trim().length > 0) @@ -69,3 +58,30 @@ export async function getFilesForCommit() { .filter(Boolean) ); } + +/** + * Get the files that are staged for commit (excluding deleted files) + * as `File` objects that are aware of their commit status. + * + * @return {Promise>} + */ +export async function getFilesForCommit() { + const simpleGit = new SimpleGit(REPO_ROOT); + + const staged = await fcb((cb) => simpleGit.diff(['--name-status', '--cached'], cb)); // staged + + return getFileList(staged); +} + +/** + * Get the unstaged files as `File` objects that are aware of their commit status. + * + * @return {Promise>} + */ +export async function getUnstagedFiles() { + const simpleGit = new SimpleGit(REPO_ROOT); + + const unstaged = await fcb((cb) => simpleGit.diff(['--name-status'], cb)); + + return getFileList(unstaged); +} diff --git a/src/dev/precommit_hook/index.js b/src/dev/precommit_hook/index.js index 14bf1af2a34b..898b4393d860 100644 --- a/src/dev/precommit_hook/index.js +++ b/src/dev/precommit_hook/index.js @@ -29,4 +29,5 @@ */ export { checkFileCasing } from './check_file_casing'; -export { getFilesForCommit } from './get_files_for_commit'; +export { getFilesForCommit, getUnstagedFiles } from './get_files_for_commit'; +export { checkDevDocs } from './check_dev_docs'; diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js index 86a279166aca..b840fca99752 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -31,13 +31,26 @@ import { run, combineErrors } from '@osd/dev-utils'; import * as Eslint from './eslint'; import * as Stylelint from './stylelint'; -import { getFilesForCommit, checkFileCasing } from './precommit_hook'; +import { + getFilesForCommit, + getUnstagedFiles, + checkFileCasing, + checkDevDocs, +} from './precommit_hook'; run( async ({ log, flags }) => { const files = await getFilesForCommit(); + const unstagedFiles = await getUnstagedFiles(); const errors = []; + try { + // Check if the dev docs sidebar has been updated but not staged + await checkDevDocs(log, unstagedFiles); + } catch (error) { + errors.push(error); + } + try { await checkFileCasing(log, files); } catch (error) { From 9971d4f7005a75b184ae78f9f6fa384aede33e73 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Tue, 14 May 2024 20:52:29 -0700 Subject: [PATCH 24/27] Move @BSFishy to emeritus maintainer (#6790) * Move @BSFishy to emeritus maintainer Signed-off-by: Lu Yu * Changeset file for PR #6790 created/updated --------- Signed-off-by: Lu Yu Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- .github/CODEOWNERS | 2 +- MAINTAINERS.md | 2 +- changelogs/fragments/6790.yml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/6790.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 88e62892d9a9..4cece3b8f608 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @ananzh @kavilla @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @BSFishy @curq @bandinib-amzn @SuZhou-Joe @ruanyl @BionIT @xinruiba @zhyuanqi +* @ananzh @kavilla @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @curq @bandinib-amzn @SuZhou-Joe @ruanyl @BionIT @xinruiba @zhyuanqi diff --git a/MAINTAINERS.md b/MAINTAINERS.md index d6155efcd837..fcd92f0eb239 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -17,7 +17,6 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Manasvini B Suryanarayana | [manasvinibs](https://github.com/manasvinibs) | Amazon | | Tao Liu | [Flyingliuhub](https://github.com/Flyingliuhub) | Amazon | | Zilong Xia | [ZilongX](https://github.com/ZilongX) | Amazon | -| Matt Provost | [BSFishy](https://github.com/BSFishy) | Amazon | | Sirazh Gabdullin | [curq](https://github.com/curq) | External contributor | | Bandini Bhopi | [bandinib-amzn](https://github.com/bandinib-amzn) | Amazon | | Su Zhou | [SuZhou-Joe](https://github.com/SuZhou-Joe) | Amazon | @@ -35,3 +34,4 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Bishoy Boktor | [boktorbb](https://github.com/boktorbb) | Amazon | | Sean Neumann | [seanneumann](https://github.com/seanneumann) | Contributor | | Kristen Tian | [kristenTian](https://github.com/kristenTian) | Amazon | +| Matt Provost | [BSFishy](https://github.com/BSFishy) | Amazon | diff --git a/changelogs/fragments/6790.yml b/changelogs/fragments/6790.yml new file mode 100644 index 000000000000..afd05b60c082 --- /dev/null +++ b/changelogs/fragments/6790.yml @@ -0,0 +1,2 @@ +doc: +- Move @BSFishy to emeritus maintainer ([#6790](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6790)) \ No newline at end of file From f50addd68bb87686fe4da18f6526aa80fed8ce35 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Wed, 15 May 2024 15:33:32 -0700 Subject: [PATCH 25/27] add @mengweieric as maintainer (#6798) * add @mengweieric as maintainer Signed-off-by: Lu Yu * Changeset file for PR #6798 created/updated --------- Signed-off-by: Lu Yu Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- .github/CODEOWNERS | 2 +- MAINTAINERS.md | 1 + changelogs/fragments/6798.yml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/6798.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4cece3b8f608..bf58f69e1c83 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @ananzh @kavilla @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @curq @bandinib-amzn @SuZhou-Joe @ruanyl @BionIT @xinruiba @zhyuanqi +* @ananzh @kavilla @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @curq @bandinib-amzn @SuZhou-Joe @ruanyl @BionIT @xinruiba @zhyuanqi @mengweieric diff --git a/MAINTAINERS.md b/MAINTAINERS.md index fcd92f0eb239..7f7dff5d37fe 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -24,6 +24,7 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Lu Yu | [BionIT](https://github.com/BionIT) | Amazon | | Xinrui Bai | [xinruiba](https://github.com/xinruiba) | Amazon | | Ella Zhu | [zhyuanqi](https://github.com/zhyuanqi) | Amazon | +| Eric Wei | [mengweieric](https://github.com/mengweieric) | Amazon | ## Emeritus diff --git a/changelogs/fragments/6798.yml b/changelogs/fragments/6798.yml new file mode 100644 index 000000000000..de03d466154b --- /dev/null +++ b/changelogs/fragments/6798.yml @@ -0,0 +1,2 @@ +doc: +- Add mengweieric as maintainer ([#6798](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6798)) \ No newline at end of file From f7130d2ce12bea0f5be6f4c69f060529016680cc Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Wed, 15 May 2024 16:58:51 -0700 Subject: [PATCH 26/27] Add OpenAPI specification for get and create saved object APIs (#6799) * add openapi doc Signed-off-by: Lu Yu * add readme Signed-off-by: Lu Yu * Changeset file for PR #6799 created/updated --------- Signed-off-by: Lu Yu Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6799.yml | 2 + docs/openapi/README.md | 9 ++ docs/openapi/saved_objects/index.html | 29 +++++ docs/openapi/saved_objects/saved_objects.yml | 130 +++++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 changelogs/fragments/6799.yml create mode 100644 docs/openapi/README.md create mode 100644 docs/openapi/saved_objects/index.html create mode 100644 docs/openapi/saved_objects/saved_objects.yml diff --git a/changelogs/fragments/6799.yml b/changelogs/fragments/6799.yml new file mode 100644 index 000000000000..0fc28064724d --- /dev/null +++ b/changelogs/fragments/6799.yml @@ -0,0 +1,2 @@ +doc: +- Add OpenAPI specification for GET and CREATE saved object API ([#6799](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6799)) \ No newline at end of file diff --git a/docs/openapi/README.md b/docs/openapi/README.md new file mode 100644 index 000000000000..a19b2a9a830a --- /dev/null +++ b/docs/openapi/README.md @@ -0,0 +1,9 @@ +## OpenAPI Specification For OpenSearch Dashboards API + +### OpenAPI +The OpenAPI (https://swagger.io/specification/) Specification defines a standard, language-agnostic interface to the HTTP RESTful APIs which allows both humans and computers to discover and understand the functionalities provided by the service without having to read through the source code or lengthy documentation. When properly defined, a consumer of the API can understand and interact with the service with a minimal amount of efforts. The OpenAPI definition file can be in the YAML or JSON format. + +When generated, OpenAPI definition can then be used by documentation generation tools to display the API such as swagger UI, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases. + +### Starting Up the Swagger UI Locally +To start up the swagger UI locally for development or validation purposes, you can simply start a server in the directory where the index.html file is located. `npx serve` is a simple way to start a server. \ No newline at end of file diff --git a/docs/openapi/saved_objects/index.html b/docs/openapi/saved_objects/index.html new file mode 100644 index 000000000000..023837592c4d --- /dev/null +++ b/docs/openapi/saved_objects/index.html @@ -0,0 +1,29 @@ + + + + + + + + Saved Object API + + + +
+ + + + + + \ No newline at end of file diff --git a/docs/openapi/saved_objects/saved_objects.yml b/docs/openapi/saved_objects/saved_objects.yml new file mode 100644 index 000000000000..3d50114c2c2c --- /dev/null +++ b/docs/openapi/saved_objects/saved_objects.yml @@ -0,0 +1,130 @@ +openapi: 3.0.3 +info: + version: v1 + title: OpenSearch Dashboards Saved Objects API + contact: + name: OpenSearch Dashboards Team + description: |- + OpenAPI schema for OpenSearch Dashboards Saved Objects API +tags: + - name: saved objects + description: Manage Dashboards saved objects, including dashboards, visualizations, saved search, and more. +paths: + /api/saved_objects/{type}/{id}: + get: + tags: + - saved objects + summary: Retrieve a single saved object by type and id. + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/type' + responses: + '200': + description: The saved object is successfully retrieved. + content: + application/json: + schema: + type: object + '404': + description: The saved object does not exist. + content: + application/json: + schema: + type: object + post: + tags: + - saved objects + summary: Create a new saved object with type and id. + parameters: + - $ref: '#components/parameters/type' + - $ref: '#components/parameters/id' + - in: query + name: overwrite + description: If set to true, will overwrite the existing saved object with same type and id. + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - attributes + properties: + attributes: + type: object + description: The metadata of the saved object to be created, and the object is not validated. + migrationVersion: + type: object + description: The information about the migrations that have been applied to this saved object to be created. + references: + description: List of objects that describe other saved objects the created object references. + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + type: + type: string + initialNamespaces: + description: Namespaces that this saved object exists in. This attribute is only used for multi-namespace saved object types. + type: array + items: + type: string + workspaces: + type: array + items: + type: string + description: Workspaces that this saved object exists in. + responses: + '200': + description: The creation request is successful + content: + application/json: + schema: + type: object + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/400_bad_request' +components: + parameters: + type: + name: type + in: path + description: The type of SavedObject to retrieve + required: true + schema: + type: string + id: + name: id + in: path + description: Unique id of the saved object. + required: true + schema: + type: string + schemas: + 400_bad_request: + title: Bad request + type: object + required: + - error + - message + - statusCode + properties: + error: + type: string + enum: + - Bad Request + message: + type: string + statusCode: + type: integer + enum: + - 400 \ No newline at end of file From 09d09a0e9e742e2f916537ca491990ac7c49df24 Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Thu, 16 May 2024 14:07:34 -0700 Subject: [PATCH 27/27] [Multiple Datasource Test]Add test for toast button and validation form (#6755) * add test for toast button and validation form Signed-off-by: yujin-emma * Changeset file for PR #6755 created/updated Signed-off-by: yujin-emma * Update src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx Co-authored-by: Lu Yu Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Signed-off-by: yujin-emma * Update manage_data_source_button.test.tsx Signed-off-by: yujin-emma * [Multiple Datasource Test] Add test for edit data source form (#6742) * add test for edit data source form Signed-off-by: yujin-emma * Changeset file for PR #6742 created/updated --------- Signed-off-by: yujin-emma Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma * [MQL] support enhancing language selector (#6613) Enable with `data.enhancements.enabled: true` Allows for enhancing the data plugin UI service and search service. #### Remaining work * Address issue with time range being invalid if previous state successfully queried and set it with a time range format that is invalid for the new query language * For example, DQL with quick time range (4 weeks to now), get results. Switch to PPL, even though PPL has a default time range enhancement. The props date range saved in the app state takes priority and sets the time range to quick range causing an error. I can still modify the time range and get a successful query but it will first fail until the user updates it to a non quick time range. * Add tests * Disable for plugins that do not support the functionality * By default index patterns are created with a unique ID. However, it can be enabled to create an index pattern with a custom ID that matches the name of the index pattern (which in turn maps to indices). * For seamless integration, the temp data frame would need to check if the index pattern that maps to the data frame name. And get it's id. * This means that dashboards with visualizations that were created with an index pattern unique ID still require the existing index pattern to exist in memory. ### Issues Resolved closes #6639 closes #6311 partially resolves: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5504 * add error data frame Signed-off-by: Paul Sebastian move language to left, some styling and disable per app name Signed-off-by: Kawika Avilla --------- Signed-off-by: Kawika Avilla Signed-off-by: Paul Sebastian Co-authored-by: Paul Sebastian Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma * Make Field Name Search Filter Case Insensitive (#6759) * Make Field Name Filter Case Insensitive Signed-off-by: Suchit Sahoo * Changeset file for PR #6759 created/updated --------- Signed-off-by: Suchit Sahoo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma * address naming for manage data source button test id Signed-off-by: yujin-emma --------- Signed-off-by: yujin-emma Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Signed-off-by: Kawika Avilla Signed-off-by: Paul Sebastian Signed-off-by: Suchit Sahoo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Lu Yu Co-authored-by: Kawika Avilla Co-authored-by: Paul Sebastian Co-authored-by: Suchit Sahoo <38322563+LDrago27@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6755.yml | 2 + .../manage_data_source_button.test.tsx | 38 ++++++ .../manage_data_source_button.tsx | 7 +- .../toast_button/reload_button.test.tsx | 26 ++++ .../datasource_form_validation.test.ts | 116 +++++++++++++++++- .../data_source_management/public/mocks.ts | 1 + 6 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/6755.yml create mode 100644 src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.test.tsx create mode 100644 src/plugins/data_source_management/public/components/toast_button/reload_button.test.tsx diff --git a/changelogs/fragments/6755.yml b/changelogs/fragments/6755.yml new file mode 100644 index 000000000000..8e06db889066 --- /dev/null +++ b/changelogs/fragments/6755.yml @@ -0,0 +1,2 @@ +fix: +- Add test for toast button and validation form ([#6755](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6755)) \ No newline at end of file diff --git a/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.test.tsx b/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.test.tsx new file mode 100644 index 000000000000..d61cd0dcabc7 --- /dev/null +++ b/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { getManageDataSourceButton } from './manage_data_source_button'; +import { coreMock } from '../../../../../core/public/mocks'; +import { DSM_APP_ID } from '../../plugin'; +import { render } from '@testing-library/react'; + +describe('ManageDataSourceButton', () => { + const applicationMock = coreMock.createStart().application; + + it('renders without crashing', () => { + const wrapper = render(getManageDataSourceButton()); + expect(wrapper).toBeTruthy(); + }); + + it('renders a button with correct label', () => { + const { getByTestId } = render(getManageDataSourceButton(applicationMock)); + const container = getByTestId('manageDataSourceButtonContainer'); + expect(container).toBeInTheDocument(); + expect(container).toHaveTextContent('Manage data sources'); + }); + + it('navigates to management app on button click', () => { + const { getByTestId } = render(getManageDataSourceButton(applicationMock)); + const button = getByTestId('manageDataSourceButton'); + button.click(); + expect(applicationMock.navigateToApp).toHaveBeenCalledTimes(1); + + expect(applicationMock.navigateToApp).toHaveBeenCalledWith('management', { + path: `opensearch-dashboards/${DSM_APP_ID}`, // Assuming DSM_APP_ID is replaced with a value + }); + }); +}); diff --git a/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx b/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx index 6222f74fdec4..63fcab2fdf70 100644 --- a/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx +++ b/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx @@ -11,9 +11,14 @@ import { DSM_APP_ID } from '../../plugin'; export const getManageDataSourceButton = (application?: ApplicationStart) => { return ( <> - + application?.navigateToApp('management', { diff --git a/src/plugins/data_source_management/public/components/toast_button/reload_button.test.tsx b/src/plugins/data_source_management/public/components/toast_button/reload_button.test.tsx new file mode 100644 index 000000000000..31223ae6943f --- /dev/null +++ b/src/plugins/data_source_management/public/components/toast_button/reload_button.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render, fireEvent } from '@testing-library/react'; +import { getReloadButton } from './reload_button'; + +describe('getReloadButton', () => { + it('renders button with correct label', () => { + const { getByText } = render(getReloadButton()); + expect(getByText('Refresh the page')).toBeInTheDocument(); + }); + + it('calls window.location.reload() on button click', () => { + const reloadMock = jest.fn(); + Object.defineProperty(window, 'location', { + value: { reload: reloadMock }, + writable: true, + }); + + const { getByText } = render(getReloadButton()); + fireEvent.click(getByText('Refresh the page')); + expect(reloadMock).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts index 7b92a355e908..925185b98b6d 100644 --- a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts +++ b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts @@ -7,11 +7,14 @@ import { AuthType } from '../../types'; import { CreateDataSourceState } from '../create_data_source_wizard/components/create_form/create_data_source_form'; import { EditDataSourceState } from '../edit_data_source/components/edit_form/edit_data_source_form'; import { defaultValidation, performDataSourceFormValidation } from './datasource_form_validation'; -import { mockDataSourceAttributesWithAuth } from '../../mocks'; +import { + mockDataSourceAttributesWithAuth, + mockDataSourceAttributesWithSigV4Auth, +} from '../../mocks'; import { AuthenticationMethod, AuthenticationMethodRegistry } from '../../auth_registry'; describe('DataSourceManagement: Form Validation', () => { - describe('validate create/edit datasource', () => { + describe('validate create/edit datasource for Username and Password auth type', () => { let authenticationMethodRegistry = new AuthenticationMethodRegistry(); let form: CreateDataSourceState | EditDataSourceState = { formErrorsByField: { ...defaultValidation }, @@ -117,4 +120,113 @@ describe('DataSourceManagement: Form Validation', () => { expect(result).toBe(true); }); }); + + describe('validate create/edit datasource for SigV4 auth type', () => { + let authenticationMethodRegistry = new AuthenticationMethodRegistry(); + let form: CreateDataSourceState | EditDataSourceState = { + formErrorsByField: { ...defaultValidation }, + title: '', + description: '', + endpoint: '', + auth: { + type: AuthType.SigV4, + credentials: { + accesskey: 'test123', + secretKey: 'test123', + service: 'es', + region: 'us-east-1', + }, + }, + }; + test('should fail validation when title is empty', () => { + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistry); + expect(result).toBe(false); + }); + test('should fail validation on duplicate title', () => { + form.title = 'test'; + const result = performDataSourceFormValidation( + form, + ['oldTitle', 'test'], + 'oldTitle', + authenticationMethodRegistry + ); + expect(result).toBe(false); + }); + test('should fail validation when title is longer than 32 characters', () => { + form.title = 'test'.repeat(10); + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistry); + expect(result).toBe(false); + }); + test('should fail validation when endpoint is not valid', () => { + form.endpoint = mockDataSourceAttributesWithSigV4Auth.endpoint; + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistry); + expect(result).toBe(false); + }); + test('should fail validation when accesskey is empty', () => { + form.auth.credentials!.accessKey = 'test'; + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistry); + expect(result).toBe(false); + }); + test('should fail validation when secrectKey is empty', () => { + form.auth.credentials!.accessKey = 'test'; + form.auth.credentials!.secretKey = ''; + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistry); + expect(result).toBe(false); + }); + test('should NOT fail validation on empty accesskey/secretKey when No Auth is selected', () => { + form.auth.type = AuthType.NoAuth; + form.title = 'test'; + form.endpoint = mockDataSourceAttributesWithSigV4Auth.endpoint; + const result = performDataSourceFormValidation(form, [], '', authenticationMethodRegistry); + expect(result).toBe(true); + }); + test('should NOT fail validation on all fields', () => { + form = { ...form, ...mockDataSourceAttributesWithSigV4Auth }; + const result = performDataSourceFormValidation( + form, + [mockDataSourceAttributesWithSigV4Auth.title], + mockDataSourceAttributesWithSigV4Auth.title, + authenticationMethodRegistry + ); + expect(result).toBe(true); + }); + test('should NOT fail validation when registered auth type is selected and related credential field not empty', () => { + authenticationMethodRegistry = new AuthenticationMethodRegistry(); + const authMethodToBeTested = { + name: 'Some Auth Type', + credentialSourceOption: { + value: 'Some Auth Type', + inputDisplay: 'some input', + }, + credentialForm: jest.fn(), + credentialFormField: { + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: 'some filled in password from registed auth credential form', + }, + } as AuthenticationMethod; + + authenticationMethodRegistry.registerAuthenticationMethod(authMethodToBeTested); + + const formWithRegisteredAuth: CreateDataSourceState | EditDataSourceState = { + formErrorsByField: { ...defaultValidation }, + title: 'test registered auth type', + description: '', + endpoint: 'https://test.com', + auth: { + type: 'Some Auth Type', + credentials: { + userNameRegistered: 'some filled in userName from registed auth credential form', + passWordRegistered: 'some filled in password from registed auth credential form', + }, + }, + }; + const result = performDataSourceFormValidation( + formWithRegisteredAuth, + [], + '', + authenticationMethodRegistry + ); + expect(result).toBe(true); + }); + }); }); diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index 0e5ec60bc307..a33a55d6799c 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -278,6 +278,7 @@ export const mockDataSourceAttributesWithSigV4Auth = { accessKey: 'test123', secretKey: 'test123', region: 'us-east-1', + service: 'es', }, }, };