diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index 2dce15fd35a1..a1521c43be5d 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -93,6 +93,14 @@ jobs: - group: 13 config: query_enhanced test_location: source + # Dashboard tests with query enhanced - group 4 + - group: 14 + config: query_enhanced + test_location: source + # Dashboard tests with query enhanced - group 5 + - group: 15 + config: query_enhanced + test_location: source container: image: docker://opensearchstaging/ci-runner:ci-runner-rockylinux8-opensearch-dashboards-integtest-v2 options: --user 1001 diff --git a/changelogs/fragments/9152.yml b/changelogs/fragments/9152.yml new file mode 100644 index 000000000000..4972a4d7ca2c --- /dev/null +++ b/changelogs/fragments/9152.yml @@ -0,0 +1,2 @@ +feat: +- Vega visualization with ppl now supports reading time field ([#9152](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9152)) \ No newline at end of file diff --git a/changelogs/fragments/9322.yml b/changelogs/fragments/9322.yml new file mode 100644 index 000000000000..e73b488f1a1f --- /dev/null +++ b/changelogs/fragments/9322.yml @@ -0,0 +1,2 @@ +test: +- [TESTID-64] Add cypress test for auto query updates when switch dataset ([#9322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9322)) \ No newline at end of file diff --git a/changelogs/fragments/9344.yml b/changelogs/fragments/9344.yml new file mode 100644 index 000000000000..15fc182311d6 --- /dev/null +++ b/changelogs/fragments/9344.yml @@ -0,0 +1,2 @@ +chore: +- Update query editor loading UI ([#9344](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9344)) \ No newline at end of file diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/a_check.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/a_check.spec.js index 1ebedef10312..94f035263b7b 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/a_check.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/a_check.spec.js @@ -18,7 +18,6 @@ const noIndexPatternTestSuite = () => { [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] ); - // Add data source cy.osd.addDataSource({ name: DATASOURCE_NAME, diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/advanced_settings.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/advanced_settings.spec.js new file mode 100644 index 000000000000..2aea9d50eecd --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/advanced_settings.spec.js @@ -0,0 +1,145 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + INDEX_WITH_TIME_2, + PATHS, + DATASOURCE_NAME, + DatasetTypes, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + setDatePickerDatesAndSearchIfRelevant, + generateBaseConfiguration, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { prepareTestSuite } from '../../../../../utils/helpers'; +import { generateQueryTestConfigurations } from '../../../../../utils/apps/query_enhancements/queries'; +import { getDatasetName } from '../../../../../utils/apps/query_enhancements/autocomplete'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runAdvancedSettingsTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_2}.mapping.json`, + ], + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_2}.data.ndjson`, + ] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteWorkspaceByName(workspaceName); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + cy.osd.deleteIndex(INDEX_WITH_TIME_2); + }); + + generateQueryTestConfigurations(generateBaseConfiguration).forEach((config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === DatasetTypes.INDEX_PATTERN.name) { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_2, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('ignoreFilterIfFieldNotInIndex should affect filtered field', () => { + // Default courier:ignoreFilterIfFieldNotInIndex should be false + // This is to ensure the setting is not changed by other tests + cy.setAdvancedSetting({ + 'courier:ignoreFilterIfFieldNotInIndex': false, + }); + // Get dataset names based on type + const firstDataset = getDatasetName(INDEX_WITH_TIME_1, config.datasetType); + const secondDataset = getDatasetName(INDEX_WITH_TIME_2, config.datasetType); + cy.setDataset(firstDataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + + // courier:ignoreFilterIfFieldNotInIndex is off + cy.submitFilterFromDropDown('unique_category', 'is', 'Caching', true); + cy.verifyHitCount(500); + + cy.setDataset(secondDataset, DATASOURCE_NAME, config.datasetType); + cy.getElementByTestId('discoverNoResults').should('exist'); + + // Turn on courier:ignoreFilterIfFieldNotInIndex + cy.setAdvancedSetting({ + 'courier:ignoreFilterIfFieldNotInIndex': true, + }); + cy.reload(); + cy.getElementByTestId('discoverNoResults').should('not.exist'); + cy.verifyHitCount('10,000'); + cy.setAdvancedSetting({ + 'courier:ignoreFilterIfFieldNotInIndex': false, + }); + }); + + it('sampleSize should affect the number of hits', () => { + // Default discover:sampleSize is 500 + // This is to ensure the setting is not changed by other tests + cy.setAdvancedSetting({ + 'discover:sampleSize': 500, + }); + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + + // default discover:sampleSize is 500 + // discover shows 50 hits per page by default + cy.getElementByTestId('docTableField').should('have.length', 100); + + // Turn on courier:sampleSize + cy.setAdvancedSetting({ + 'discover:sampleSize': 5, + }); + cy.reload(); + // Should not affect total hits + cy.verifyHitCount('10,000'); + cy.getElementByTestId('docTableField').should('have.length', 10); + cy.setAdvancedSetting({ + 'discover:sampleSize': 500, + }); + }); + }); + }); + }); +}; + +prepareTestSuite('Advanced Settings', runAdvancedSettingsTests); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_query.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_query.spec.js new file mode 100644 index 000000000000..b4bafe06c621 --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_query.spec.js @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + QueryLanguages, + PATHS, + DATASOURCE_NAME, + DatasetTypes, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + setDatePickerDatesAndSearchIfRelevant, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { + validateQueryResults, + generateAutocompleteTestConfiguration, + generateAutocompleteTestConfigurations, + createOtherQueryUsingAutocomplete, + createDQLQueryUsingAutocomplete, +} from '../../../../../utils/apps/query_enhancements/autocomplete'; +import { prepareTestSuite } from '../../../../../utils/helpers'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runAutocompleteTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteWorkspaceByName(workspaceName); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + }); + + generateAutocompleteTestConfigurations(generateAutocompleteTestConfiguration).forEach( + (config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === DatasetTypes.INDEX_PATTERN.name) { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('should show and select suggestions progressively', () => { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + + if (config.language === QueryLanguages.DQL.name) { + createDQLQueryUsingAutocomplete(); + } else { + createOtherQueryUsingAutocomplete(config); + } + + // Run the query + cy.getElementByTestId('querySubmitButton').click(); + cy.waitForLoader(true); + cy.wait(1000); + // Validate results meet our conditions + validateQueryResults('bytes_transferred', 9500, '>'); + validateQueryResults('category', 'Application'); + }); + }); + } + ); + }); +}; + +prepareTestSuite('Autocomplete Query', runAutocompleteTests); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_switch.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_switch.spec.js new file mode 100644 index 000000000000..40b8701dd33b --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_switch.spec.js @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + INDEX_WITH_TIME_2, + QueryLanguages, + PATHS, + DATASOURCE_NAME, + DatasetTypes, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + getDefaultQuery, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { + generateAutocompleteTestConfiguration, + generateAutocompleteTestConfigurations, + LanguageConfigs, + getDatasetName, +} from '../../../../../utils/apps/query_enhancements/autocomplete'; +import { prepareTestSuite } from '../../../../../utils/helpers'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runAutocompleteTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_2}.mapping.json`, + ], + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_2}.data.ndjson`, + ] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteWorkspaceByName(workspaceName); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + cy.osd.deleteIndex(INDEX_WITH_TIME_2); + }); + + generateAutocompleteTestConfigurations(generateAutocompleteTestConfiguration, { + languageConfig: LanguageConfigs.SQL_PPL, + }).forEach((config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === DatasetTypes.INDEX_PATTERN.name) { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_2, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('should update default query when switching index patterns and languages', () => { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + + // Get dataset names based on type + const firstDataset = getDatasetName('data_logs_small_time_1', config.datasetType); + const secondDataset = getDatasetName('data_logs_small_time_2', config.datasetType); + + // Verify initial default query + cy.getElementByTestId('osdQueryEditor__multiLine').contains( + getDefaultQuery(firstDataset, config.language) + ); + + // Switch to second index pattern + cy.setDataset(secondDataset, DATASOURCE_NAME, config.datasetType); + + // Verify query updated for new index pattern + cy.getElementByTestId('osdQueryEditor__multiLine').contains( + getDefaultQuery(secondDataset, config.language) + ); + + // Switch language and verify index pattern maintained + const switchLanguage = + config.language === QueryLanguages.SQL.name + ? QueryLanguages.PPL.name + : QueryLanguages.SQL.name; + cy.setQueryLanguage(switchLanguage); + cy.getElementByTestId('osdQueryEditor__multiLine').contains( + getDefaultQuery(secondDataset, switchLanguage) + ); + }); + }); + }); + }); +}; + +prepareTestSuite('Autocomplete Switch', runAutocompleteTests); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_ui.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_ui.spec.js new file mode 100644 index 000000000000..6f0d7e90db2f --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_ui.spec.js @@ -0,0 +1,151 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + QueryLanguages, + PATHS, + DATASOURCE_NAME, + DatasetTypes, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + setDatePickerDatesAndSearchIfRelevant, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { + generateAutocompleteTestConfiguration, + generateAutocompleteTestConfigurations, + validateQueryResults, + showSuggestionAndHint, + hideWidgets, + createQuery, +} from '../../../../../utils/apps/query_enhancements/autocomplete'; +import { prepareTestSuite } from '../../../../../utils/helpers'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runAutocompleteTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteWorkspaceByName(workspaceName); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + cy.window().then((win) => { + win.localStorage.clear(); + win.sessionStorage.clear(); + }); + }); + + generateAutocompleteTestConfigurations(generateAutocompleteTestConfiguration).forEach( + (config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === DatasetTypes.INDEX_PATTERN.name) { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('should verify suggestion widget and its hint', () => { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + const editorType = + config.language === QueryLanguages.DQL.name + ? 'osdQueryEditor__singleLine' + : 'osdQueryEditor__multiLine'; + + cy.getElementByTestId(editorType) + .find('.monaco-editor') + .should('be.visible') + .within(() => { + // Show suggestion and hint with retry + showSuggestionAndHint(); + + // Verify suggestions are visible + cy.get('.monaco-list-row').should('be.visible').should('have.length.at.least', 1); + + // Sends ESC and verifies widgets are hidden + hideWidgets(); + + // TODO: Add test for having another focused window after bug is fixed + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8973 + }); + }); + + it('should build query using mouse interactions', () => { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + + createQuery(config, false); // use mouse + + // Run with mouse click + cy.getElementByTestId('querySubmitButton').click(); + + cy.waitForLoader(true); + cy.wait(1000); + validateQueryResults('unique_category', 'Configuration'); + }); + + it('should build query using keyboard shortcuts', () => { + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + + createQuery(config, true); // use keyboard + + // Run with keyboard shortcut + if (config.language === QueryLanguages.DQL.name) { + cy.get('.inputarea').type('{enter}'); + } else { + // SQL and PPL should use cy.get('.inputarea').type('{cmd+enter}') + // But it is not working in Remote CI + // TODO: investigate and fix + cy.getElementByTestId('querySubmitButton').click(); + } + + cy.waitForLoader(true); + cy.wait(2000); + validateQueryResults('unique_category', 'Configuration'); + }); + }); + } + ); + }); +}; + +prepareTestSuite('Autocomplete UI', runAutocompleteTests); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/caching.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/caching.spec.js index 3f1713bc9ea0..20d3cbfb8d3e 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/caching.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/caching.spec.js @@ -20,7 +20,6 @@ const cachingTestSuite = () => { ['cypress/fixtures/query_enhancements/data_logs_1/data_logs_small_time_1.mapping.json'], ['cypress/fixtures/query_enhancements/data_logs_1/data_logs_small_time_1.data.ndjson'] ); - // Add data source cy.osd.addDataSource({ name: DATASOURCE_NAME, diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/dataset_selector.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/dataset_selector.spec.js index 722dfb7755d6..817721b69aa2 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/dataset_selector.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/dataset_selector.spec.js @@ -9,6 +9,7 @@ import { INDEX_WITH_TIME_1, INDEX_WITH_TIME_2, PATHS, + DatasetTypes, } from '../../../../../utils/constants'; import { @@ -50,7 +51,6 @@ export const runDatasetSelectorTests = () => { url: PATHS.SECONDARY_ENGINE, authType: 'no_auth', }); - // Create workspace cy.deleteAllWorkspaces(); cy.visit('/app/home'); @@ -79,7 +79,7 @@ export const runDatasetSelectorTests = () => { isEnhancement: true, }); - if (config.datasetType === 'INDEX_PATTERN') { + if (config.datasetType === DatasetTypes.INDEX_PATTERN.name) { cy.setIndexPatternFromAdvancedSelector(config.dataset, DATASOURCE_NAME, config.language); } else { cy.setIndexAsDataset(config.dataset, DATASOURCE_NAME, config.language); @@ -111,7 +111,7 @@ export const runDatasetSelectorTests = () => { verifyBaseState(INDEX_PATTERN_WITH_TIME); // Try setting the dataset-language combination but click on cancel - if (config.datasetType === 'INDEX_PATTERN') { + if (config.datasetType === DatasetTypes.INDEX_PATTERN.name) { cy.setIndexPatternFromAdvancedSelector( config.dataset, DATASOURCE_NAME, diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js index 695ddb784048..5b11f4709477 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js @@ -25,7 +25,6 @@ const fieldDisplayFilteringTestSuite = () => { ['cypress/fixtures/query_enhancements/data_logs_1/data_logs_small_time_1.mapping.json'], ['cypress/fixtures/query_enhancements/data_logs_1/data_logs_small_time_1.data.ndjson'] ); - // Add data source cy.osd.addDataSource({ name: DATASOURCE_NAME, diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/inspect.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/inspect.spec.js index 2154016e8344..f97753998169 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/inspect.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/inspect.spec.js @@ -39,7 +39,6 @@ const inspectTestSuite = () => { [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] ); - // Add data source cy.osd.addDataSource({ name: DATASOURCE_NAME, diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/queries.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/queries.spec.js index f12c58b16fbd..39bc3f24c83e 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/queries.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/queries.spec.js @@ -24,19 +24,16 @@ const queriesTestSuite = () => { [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] ); - // Add data source cy.osd.addDataSource({ name: DATASOURCE_NAME, url: PATHS.SECONDARY_ENGINE, authType: 'no_auth', }); - // Create workspace and set up index pattern cy.deleteAllWorkspaces(); cy.visit('/app/home'); cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspace); - // Create and select index pattern for ${INDEX_WITH_TIME_1}* cy.createWorkspaceIndexPatterns({ workspaceName: workspace, @@ -46,7 +43,6 @@ const queriesTestSuite = () => { dataSource: DATASOURCE_NAME, isEnhancement: true, }); - // Go to discover page cy.navigateToWorkSpaceSpecificPage({ workspaceName: workspace, diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/queries_more.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/queries_more.spec.js new file mode 100644 index 000000000000..1fb7e88f43f7 --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/queries_more.spec.js @@ -0,0 +1,126 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + INDEX_WITHOUT_TIME_1, + PATHS, + DATASOURCE_NAME, + DatasetTypes, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + setDatePickerDatesAndSearchIfRelevant, + generateBaseConfiguration, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { getDatasetName } from '../../../../../utils/apps/query_enhancements/autocomplete'; +import { + generateQueryTestConfigurations, + LanguageConfigs, +} from '../../../../../utils/apps/query_enhancements/queries'; +import { prepareTestSuite } from '../../../../../utils/helpers'; +import { getDocTableField } from '../../../../../utils/apps/query_enhancements/doc_table'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runQueryTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITHOUT_TIME_1}.mapping.json`, + ], + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITHOUT_TIME_1}.data.ndjson`, + ] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteWorkspaceByName(workspaceName); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + cy.osd.deleteIndex(INDEX_WITHOUT_TIME_1); + }); + + generateQueryTestConfigurations(generateBaseConfiguration, { + languageConfig: LanguageConfigs.DQL_Lucene, + }).forEach((config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === DatasetTypes.INDEX_PATTERN.name) { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITHOUT_TIME_1, + timefieldName: '', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + indexPatternHasTimefield: false, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('should highlight filter and query field', () => { + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + const query = `unique_category:Caching`; + cy.setQueryEditor(query); + cy.submitFilterFromDropDown('category', 'is', 'Database', true); + getDocTableField(1, 0).within(() => { + // Get all marks and verify their texts + cy.get('mark').should(($marks) => { + const texts = $marks.map((_, el) => el.textContent).get(); + expect(texts).to.include('Database'); + expect(texts).to.include('Caching'); + }); + }); + cy.verifyHitCount(500); + + // Get dataset names based on type + const noTime = getDatasetName(INDEX_WITHOUT_TIME_1, config.datasetType); + cy.setDataset(noTime, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + cy.setQueryEditor(query); + cy.submitFilterFromDropDown('category', 'is', 'Database', true); + getDocTableField(0, 0).within(() => { + // Get all marks and verify their texts + cy.get('mark').should(($marks) => { + const texts = $marks.map((_, el) => el.textContent).get(); + expect(texts).to.include('Database'); + expect(texts).to.include('Caching'); + }); + }); + cy.verifyHitCount(500); + }); + }); + }); + }); +}; + +prepareTestSuite('Queries More', runQueryTests); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries.spec.js index 02b12e987435..bae6b35c7b94 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries.spec.js @@ -123,7 +123,6 @@ const runSavedQueriesUITests = () => { url: PATHS.SECONDARY_ENGINE, authType: 'no_auth', }); - // Create workspace cy.deleteAllWorkspaces(); cy.visit('/app/home'); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_search.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_search.spec.js index b9399fb8c686..3382c6c2dc73 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_search.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_search.spec.js @@ -49,7 +49,6 @@ const runSavedSearchTests = () => { url: PATHS.SECONDARY_ENGINE, authType: 'no_auth', }); - // Create workspace cy.deleteAllWorkspaces(); cy.visit('/app/home'); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/simple_dataset_selector.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/simple_dataset_selector.spec.js index 2347ebd8d876..ee18e84cbd82 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/simple_dataset_selector.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/simple_dataset_selector.spec.js @@ -47,7 +47,6 @@ export const runSimpleDatasetSelectorTests = () => { url: PATHS.SECONDARY_ENGINE, authType: 'no_auth', }); - // Create workspace cy.deleteAllWorkspaces(); cy.visit('/app/home'); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/time_range_selection.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/time_range_selection.spec.js index b1691d39e763..f54e427a8653 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/time_range_selection.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/time_range_selection.spec.js @@ -40,7 +40,6 @@ export const runTimeRangeSelectionTests = () => { url: PATHS.SECONDARY_ENGINE, authType: 'no_auth', }); - // Create workspace cy.deleteAllWorkspaces(); cy.visit('/app/home'); diff --git a/cypress/utils/apps/query_enhancements/autocomplete.js b/cypress/utils/apps/query_enhancements/autocomplete.js new file mode 100644 index 000000000000..05c53b916e6f --- /dev/null +++ b/cypress/utils/apps/query_enhancements/autocomplete.js @@ -0,0 +1,553 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + QueryLanguages, + INDEX_WITH_TIME_1, + INDEX_PATTERN_WITH_TIME_1, + DatasetTypes, +} from './constants'; + +// ======================================= +// Common Utilities (Used across multiple autocomplete specs) +// ======================================= + +/** + * Gets the dataset name based on the dataset type + * @param {string} baseName - Base name of the dataset + * @param {string} datasetType - Type of dataset (INDEX_PATTERN or INDEXES) + * @returns {string} Formatted dataset name + */ +export const getDatasetName = (baseName, datasetType) => { + return datasetType === DatasetTypes.INDEX_PATTERN.name ? `${baseName}*` : baseName; +}; + +/** + * Types input and verifies expected suggestion appears in suggestion list + * @param {string} input - Text to type + * @param {string} expectedSuggestion - Expected suggestion to verify + */ +export const typeAndVerifySuggestion = (input, expectedSuggestion) => { + if (input) { + cy.get('.inputarea').type(input, { force: true }); + } + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row').should('exist').contains(expectedSuggestion); + }); +}; + +/** + * Finds and selects a specific suggestion from the suggestion list + * @param {string} suggestionText - Text of suggestion to select + */ +export const selectSpecificSuggestion = (suggestionText) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row').contains(suggestionText).should('be.visible').click(); + }); +}; + +/** + * Types input, verifies suggestion exists, and selects it + * @param {string} input - Text to type + * @param {string} expectedSuggestion - Expected suggestion to verify and select + */ +export const typeAndSelectSuggestion = (input, expectedSuggestion) => { + typeAndVerifySuggestion(input, expectedSuggestion); + selectSpecificSuggestion(expectedSuggestion); +}; + +/** + * Shows suggestions by clicking in the editor + */ +export const showSuggestions = () => { + cy.get('.inputarea').click(); + cy.get('.suggest-widget').should('be.visible'); +}; + +/** + * Verifies all expected values appear in suggestion list + * @param {string[]} expectedValues - Array of values to verify + */ +export const verifyFieldValues = (expectedValues) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + expectedValues.forEach((value) => { + cy.get('.monaco-list-row').should('contain', value); + }); + }); +}; + +// ======================================= +// Autocomplete Query Spec Utilities +// ======================================= + +/** + * Handles category field suggestions flow for query building + * Used in createOtherQueryUsingAutocomplete and createDQLQueryUsingAutocomplete + */ +const handleCategoryFieldSuggestions = () => { + const expectedCategoryValues = ['Application', 'Database', 'Network', 'Security']; + typeAndSelectSuggestion('c', 'category'); + typeAndVerifySuggestion('', '='); + selectSpecificSuggestion('='); + verifyFieldValues(expectedCategoryValues); + typeAndSelectSuggestion('App', 'Application'); +}; + +/** + * Gets language-specific configuration for query building + * @param {string} language - Query language + * @param {Object} config - Configuration object + * @returns {Object} Language-specific configuration + */ +const getLanguageSpecificConfig = (language, config) => { + const datasetName = getDatasetName('data_logs_small_time_1', config.datasetType); + cy.log(`Dataset name for ${language} with type ${config.datasetType}: ${datasetName}`); + + switch (language) { + case QueryLanguages.PPL.name: + return { + initialCommands: [ + { value: 'source', input: 's' }, + { value: '=' }, + { value: getDatasetName('data_logs_small_time_1', config.datasetType) }, + { value: '|' }, + { value: 'where', input: 'w' }, + ], + editorType: 'osdQueryEditor__multiLine', + andOperator: 'and', + }; + case QueryLanguages.SQL.name: + return { + initialCommands: [ + { value: 'SELECT', input: 's' }, + { value: '*' }, + { value: 'FROM', input: 'f' }, + { value: getDatasetName('data_logs_small_time_1', config.datasetType) }, + { value: 'WHERE', input: 'w' }, + ], + editorType: 'osdQueryEditor__multiLine', + andOperator: 'AND', + }; + default: + throw new Error(`Unsupported language: ${language}`); + } +}; + +/** + * Creates a query using autocomplete for SQL/PPL languages + * @param {Object} config - Query configuration object + */ +export const createOtherQueryUsingAutocomplete = (config) => { + const langConfig = getLanguageSpecificConfig(config.language, config); + + cy.getElementByTestId(langConfig.editorType) + .find('.monaco-editor') + .should('be.visible') + .should('have.class', 'vs') + .wait(1000) + .within(() => { + // Handle initial language-specific setup + langConfig.initialCommands.forEach((command) => { + if (command.input) { + typeAndSelectSuggestion(command.input, command.value); + } else { + typeAndVerifySuggestion('', command.value); + selectSpecificSuggestion(command.value); + } + }); + + handleCategoryFieldSuggestions(); + + // Handle common ending pattern + typeAndSelectSuggestion('a', langConfig.andOperator); + typeAndVerifySuggestion('', 'bytes_transferred'); + selectSpecificSuggestion('bytes_transferred'); + + if (config.language === QueryLanguages.SQL.name) { + // Handle operator (different for SQL vs PPL) + // TODO: SQL doesn't support operator suggestions yet except for '=' + cy.get('.inputarea').type('> ', { force: true }); + } else { + typeAndVerifySuggestion('', '>'); + selectSpecificSuggestion('>'); + } + + cy.get('.inputarea').type('9500', { force: true }); + }); +}; + +/** + * Creates a DQL query using autocomplete + */ +export const createDQLQueryUsingAutocomplete = () => { + cy.getElementByTestId('osdQueryEditor__singleLine') + .find('.monaco-editor') + .should('be.visible') + .should('have.class', 'vs') + .wait(1000) + .within(() => { + typeAndSelectSuggestion('c', 'category'); + const expectedCategoryValues = ['Application', 'Database', 'Network', 'Security']; + verifyFieldValues(expectedCategoryValues); + typeAndSelectSuggestion('App', 'Application'); + typeAndSelectSuggestion('a', 'and'); + typeAndVerifySuggestion('', 'bytes_transferred'); + selectSpecificSuggestion('bytes_transferred'); + typeAndVerifySuggestion('', '>'); + selectSpecificSuggestion('>'); + cy.get('.inputarea').type('9500', { force: true }); + }); +}; + +/** + * Clicks to select a specific suggestion from the suggestion list + * Used by: autocomplete_query.spec.js + * @param {string} suggestionText - Text of suggestion to select + */ +export const clickSuggestion = (suggestionText) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row').contains(suggestionText).should('be.visible').click(); + }); +}; + +/** + * Shows suggestions and clicks to select one + * Used by: autocomplete_query.spec.js + * @param {string} expectedSuggestion - Suggestion to select + */ +export const showAndClickSuggestion = (expectedSuggestion) => { + showSuggestions(); + clickSuggestion(expectedSuggestion); +}; + +/** + * Shows suggestion and scrolls if needed before clicking + * Used by: autocomplete_query.spec.js + * @param {string} suggestionText - Text of suggestion to select + */ +export const showAndClickSuggestionWithScroll = (suggestionText) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row') + .contains(suggestionText) + .scrollIntoView() + .should('be.visible') + .click(); + }); +}; + +// ======================================= +// Autocomplete UI Spec Utilities +// ======================================= + +/** + * Selects a suggestion using keyboard or mouse with retry logic + * @param {string} suggestionText - Text of suggestion to select + * @param {boolean} useKeyboard - Whether to use keyboard instead of mouse + */ +export const selectSuggestion = (suggestionText, useKeyboard = false) => { + cy.log(`Selecting suggestion "${suggestionText}"`); + const maxAttempts = 30; + + const findAndSelectSuggestion = (attempt = 0) => { + if (attempt >= maxAttempts) { + throw new Error( + `Could not find suggestion "${suggestionText}" after ${maxAttempts} attempts` + ); + } + + return cy.get('.monaco-list-row').then(($rows) => { + const isVisible = $rows + .toArray() + .some((row) => Cypress.$(row).text().includes(suggestionText)); + + if (isVisible) { + if (useKeyboard) { + const highlightedRow = $rows.filter('.focused').text(); + if (highlightedRow.includes(suggestionText)) { + return cy.get('.inputarea').trigger('keydown', { + key: 'Tab', + keyCode: 9, + which: 9, + force: true, + }); + } + } else { + return cy.get('.monaco-list-row').contains(suggestionText).click({ force: true }); + } + } + + return cy + .get('.inputarea') + .type('{downarrow}', { force: true }) + .wait(50) + .then(() => findAndSelectSuggestion(attempt + 1)); + }); + }; + + cy.get('.suggest-widget') + .should('exist') + .should('be.visible') + .should('have.class', 'visible') + .then(() => { + findAndSelectSuggestion(0); + }); +}; + +/** + * Shows suggestion widget and waits for hint to appear with retry logic + * @param {number} maxAttempts - Maximum number of retry attempts + * @returns {Cypress.Chainable} + */ +export const showSuggestionAndHint = (maxAttempts = 3) => { + let attempts = 0; + + const attemptShow = () => { + attempts++; + cy.get('.inputarea').type(' ', { force: true }); + + return cy.get('.suggest-widget.visible').then(($widget) => { + const isVisible = $widget.is(':visible'); + const styles = window.getComputedStyle($widget[0], '::after'); + const hasHint = styles.getPropertyValue('content').includes('Tab to insert'); + + if (!isVisible || !hasHint) { + if (attempts >= maxAttempts) { + throw new Error('Failed to show suggestion and hint after ${maxAttempts} attempts'); + } + return cy.wait(200).then(attemptShow); + } + }); + }; + + return attemptShow(); +}; + +/** + * Hides suggestion widgets with retry logic + * @param {number} maxAttempts - Maximum number of retry attempts + * @returns {Cypress.Chainable} + */ +export const hideWidgets = (maxAttempts = 3) => { + let attempts = 0; + + const attemptHide = () => { + attempts++; + cy.get('.inputarea').type('{esc}', { force: true }); + + return cy.get('.suggest-widget').then(($widget) => { + if ($widget.hasClass('visible')) { + if (attempts >= maxAttempts) { + throw new Error('Failed to hide widgets after ${maxAttempts} attempts'); + } + return cy.wait(200).then(attemptHide); + } + }); + }; + + return attemptHide(); +}; + +/** + * Creates a query using either mouse or keyboard interactions + * @param {Object} config - Query configuration + * @param {boolean} useKeyboard - Whether to use keyboard instead of mouse + */ +export const createQuery = (config, useKeyboard = false) => { + const editorType = + config.language === QueryLanguages.DQL.name + ? 'osdQueryEditor__singleLine' + : 'osdQueryEditor__multiLine'; + + cy.getElementByTestId(editorType) + .find('.monaco-editor') + .should('be.visible') + .should('have.class', 'vs') + .wait(1000) + .within(() => { + cy.get('.inputarea').type(' ', { force: true }); + if (config.language === QueryLanguages.PPL.name) { + selectSuggestion('source', useKeyboard); + selectSuggestion('=', useKeyboard); + const dataset = getDatasetName('data_logs_small_time_1', config.datasetType); + selectSuggestion(dataset, useKeyboard); + selectSuggestion('|', useKeyboard); + selectSuggestion('where', useKeyboard); + selectSuggestion('unique_category', useKeyboard); + selectSuggestion('=', useKeyboard); + selectSuggestion('Configuration', useKeyboard); + } else if (config.language === QueryLanguages.SQL.name) { + selectSuggestion('SELECT', useKeyboard); + selectSuggestion('*', useKeyboard); + selectSuggestion('FROM', useKeyboard); + selectSuggestion('data_logs_small_time_1', useKeyboard); + selectSuggestion('WHERE', useKeyboard); + selectSuggestion('unique_category', useKeyboard); + selectSuggestion('=', useKeyboard); + selectSuggestion('Configuration', useKeyboard); + } else if (config.language === QueryLanguages.DQL.name) { + selectSuggestion('unique_category', useKeyboard); + selectSuggestion('Configuration', useKeyboard); + } + }); +}; + +// ======================================= +// Test Configuration Generators +// ======================================= + +/** + * Language configurations for different test scenarios + */ +export const LanguageConfigs = { + SQL_PPL: { + INDEX_PATTERN: [QueryLanguages.SQL, QueryLanguages.PPL], + INDEXES: [QueryLanguages.SQL, QueryLanguages.PPL], + }, + SQL_PPL_DQL: { + INDEX_PATTERN: [QueryLanguages.DQL, QueryLanguages.SQL, QueryLanguages.PPL], + INDEXES: [QueryLanguages.SQL, QueryLanguages.PPL], + }, +}; + +/** + * Creates dataset types configuration for autocomplete tests + * @param {Object} languageConfig - Language configuration object + * @returns {Object} Dataset types configuration + */ +const createAutocompleteDatasetTypes = (languageConfig = LanguageConfigs.SQL_PPL) => ({ + INDEX_PATTERN: { + name: 'INDEX_PATTERN', + supportedLanguages: languageConfig.INDEX_PATTERN, + }, + INDEXES: { + name: 'INDEXES', + supportedLanguages: languageConfig.INDEXES, + }, +}); + +export const AutocompleteDatasetTypes = createAutocompleteDatasetTypes(); + +// ======================================= +// Test Configuration Generators and other common utilities +// ======================================= + +/** + * Generates base test configuration for autocomplete tests + * Used by: autocomplete_query.spec.js, autocomplete_switch.spec.js, autocomplete_ui.spec.js + * @param {string} dataset - Dataset name + * @param {string} datasetType - Type of dataset + * @param {Object} language - Language configuration + * @returns {Object} Test configuration object + */ +export const generateAutocompleteTestConfiguration = (dataset, datasetType, language) => { + const baseConfig = { + dataset, + datasetType, + language: language.name, + testName: `${language.name}-${datasetType}`, + }; + + return { + ...baseConfig, + }; +}; + +/** + * Generates test configurations for autocomplete tests across different dataset types + * Used by: autocomplete_query.spec.js, autocomplete_switch.spec.js, autocomplete_ui.spec.js + * @param {Function} generateTestConfigurationCallback - Callback function to generate test config + * @param {Object} options - Configuration options + * @param {string} [options.indexPattern] - Custom index pattern name + * @param {string} [options.index] - Custom index name + * @param {Object} [options.languageConfig] - Custom language configuration + * @returns {Array} Array of test configurations + */ +export const generateAutocompleteTestConfigurations = ( + generateTestConfigurationCallback, + options = {} +) => { + const { + indexPattern = INDEX_PATTERN_WITH_TIME_1, + index = INDEX_WITH_TIME_1, + languageConfig = LanguageConfigs.SQL_PPL_DQL, + } = options; + + const datasetTypes = createAutocompleteDatasetTypes(languageConfig); + + return Object.values(datasetTypes).flatMap((dataset) => + dataset.supportedLanguages.map((language) => { + let datasetToUse; + switch (dataset.name) { + case datasetTypes.INDEX_PATTERN.name: + datasetToUse = indexPattern; + break; + case datasetTypes.INDEXES.name: + datasetToUse = index; + break; + default: + throw new Error( + `generateAutocompleteTestConfigurations encountered unsupported dataset: ${dataset.name}` + ); + } + return generateTestConfigurationCallback(datasetToUse, dataset.name, language); + }) + ); +}; + +/** + * Validates query results by comparing field values with expected values using specified operator + * Used by: autocomplete_query.spec.js, autocomplete_ui.spec.js + * @param {string} field - The field name to validate + * @param {number|string} expectedValue - The value to compare against + * @param {string} [operator] - The operator to use for comparison ('>', '<', '=', or undefined for equality) + */ +export const validateQueryResults = (field, expectedValue, operator) => { + // Expand the first row to view the field value + cy.get('tbody tr').first().find('[data-test-subj="docTableExpandToggleColumn"] button').click(); + cy.getElementByTestId(`tableDocViewRow-${field}-value`).within(() => { + cy.get('span') + .invoke('text') + .then((text) => { + // For numeric comparisons (>, <, >=, <=) + if (['>', '<', '>=', '<=', '='].includes(operator)) { + const actualValue = parseFloat(text.replace(/,/g, '')); + const numericExpectedValue = parseFloat(expectedValue.toString().replace(/,/g, '')); + + switch (operator) { + case '>': + expect(actualValue).to.be.greaterThan(numericExpectedValue); + break; + case '<': + expect(actualValue).to.be.lessThan(numericExpectedValue); + break; + case '>=': + expect(actualValue).to.be.at.least(numericExpectedValue); + break; + case '<=': + expect(actualValue).to.be.at.most(numericExpectedValue); + break; + case '=': + expect(actualValue).to.equal(numericExpectedValue); + break; + } + } else { + // For undefined, keep original string comparison + expect(text).to.equal(expectedValue.toString()); + } + }); + }); + // Close the expanded row + cy.get('tbody tr').first().find('[data-test-subj="docTableExpandToggleColumn"] button').click(); +}; diff --git a/cypress/utils/apps/query_enhancements/commands.js b/cypress/utils/apps/query_enhancements/commands.js index 5ce71f043f7e..714460985a56 100644 --- a/cypress/utils/apps/query_enhancements/commands.js +++ b/cypress/utils/apps/query_enhancements/commands.js @@ -3,7 +3,87 @@ * SPDX-License-Identifier: Apache-2.0 */ -Cypress.Commands.add('setQueryEditor', (value, opts = {}, submit = true) => { +const MAX_RETRIES = 3; +const RETRY_DELAY = 1000; + +const forceFocusEditor = () => { + return cy + .get('.globalQueryEditor .react-monaco-editor-container') + .click({ force: true }) + .wait(200) // Give editor time to register focus + .get('.inputarea') + .focus() + .wait(200); // Wait for focus to take effect +}; + +const clearMonacoEditor = () => { + return cy + .get('.globalQueryEditor .react-monaco-editor-container') + .should('exist') + .should('be.visible') + .then(() => { + // First ensure we have focus + return forceFocusEditor().then(() => { + // Try different key combinations for selection + return cy + .get('.inputarea') + .type('{ctrl}a', { force: true }) + .wait(100) + .type('{backspace}', { force: true }) + .wait(100) + .type('{meta}a', { force: true }) + .wait(100) + .type('{backspace}', { force: true }); + }); + }); +}; + +const isEditorEmpty = () => { + return cy + .get('.globalQueryEditor .react-monaco-editor-container') + .find('.view-line') + .invoke('text') + .then((text) => text.trim() === ''); +}; + +Cypress.Commands.add('clearQueryEditor', () => { + const clearWithRetry = (attempt = 1) => { + cy.log(`Attempt ${attempt} to clear editor`); + + return forceFocusEditor() + .then(() => clearMonacoEditor()) + .then(() => { + return isEditorEmpty().then((isEmpty) => { + cy.log(`is editor empty: ${isEmpty}`); + + if (isEmpty) { + return; // Editor is cleared, we're done + } + + if (attempt < MAX_RETRIES) { + cy.log(`Editor not cleared, retrying... (attempt ${attempt})`); + cy.wait(RETRY_DELAY); // Wait before next attempt + return clearWithRetry(attempt + 1); + } else { + cy.log('Failed to clear editor after all attempts'); + // Instead of throwing error, try one last time with extra waiting + return cy.wait(2000).then(forceFocusEditor).then(clearMonacoEditor); + } + }); + }); + }; + + return clearWithRetry(); +}); + +Cypress.Commands.add('setQueryEditor', (value, options = {}) => { + const defaults = { + submit: true, + }; + + // Extract our command-specific options + const { submit = defaults.submit, ...typeOptions } = options; + Cypress.log({ name: 'setQueryEditor', displayName: 'set query', @@ -15,14 +95,16 @@ Cypress.Commands.add('setQueryEditor', (value, opts = {}, submit = true) => { cy.getElementByTestId('headerGlobalNav').click(); // clear the editor first and then set - cy.get('.globalQueryEditor .react-monaco-editor-container') - .click() - .focused() - .type('{ctrl}a') - .type('{backspace}') - .type('{meta}a') - .type('{backspace}') - .type(value, opts); + clearMonacoEditor().then(() => { + return cy + .get('.inputarea') + .should('be.visible') + .wait(200) + .type(value, { + force: true, + ...typeOptions, // Pass through all other options to type command + }); + }); if (submit) { cy.updateTopNav({ log: false }); diff --git a/cypress/utils/apps/query_enhancements/index.d.ts b/cypress/utils/apps/query_enhancements/index.d.ts index d3a0e73be5f0..9f3a480aed96 100644 --- a/cypress/utils/apps/query_enhancements/index.d.ts +++ b/cypress/utils/apps/query_enhancements/index.d.ts @@ -7,8 +7,7 @@ declare namespace Cypress { interface Chainable { setQueryEditor( value: string, - opts?: { parseSpecialCharSequences?: boolean }, - submit?: boolean + options?: Partial & { submit?: boolean } ): Chainable; setQueryLanguage(value: 'DQL' | 'Lucene' | 'OpenSearch SQL' | 'PPL'): Chainable; diff --git a/cypress/utils/apps/query_enhancements/queries.js b/cypress/utils/apps/query_enhancements/queries.js new file mode 100644 index 000000000000..b5d7f91a5c96 --- /dev/null +++ b/cypress/utils/apps/query_enhancements/queries.js @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { QueryLanguages, INDEX_PATTERN_WITH_TIME_1 } from './constants'; + +// ======================================= +// Test Configuration Generators +// ======================================= + +/** + * Language configurations for different test scenarios + */ +export const LanguageConfigs = { + DQL_Lucene: { + INDEX_PATTERN: [QueryLanguages.DQL, QueryLanguages.Lucene], + }, +}; + +/** + * Creates dataset types configuration for autocomplete tests + * @param {Object} languageConfig - Language configuration object + * @returns {Object} Dataset types configuration + */ +const createQueryDatasetTypes = (languageConfig = LanguageConfigs.DQL_Lucene) => ({ + INDEX_PATTERN: { + name: 'INDEX_PATTERN', + supportedLanguages: languageConfig.INDEX_PATTERN, + }, +}); + +export const QueryDatasetTypes = createQueryDatasetTypes(); + +// ======================================= +// Test Configuration Generators and other common utilities +// ======================================= +/** + * Generates test configurations for autocomplete tests across different dataset types + * Used by: autocomplete_query.spec.js, autocomplete_switch.spec.js, autocomplete_ui.spec.js + * @param {Function} generateTestConfigurationCallback - Callback function to generate test config + * @param {Object} options - Configuration options + * @param {string} [options.indexPattern] - Custom index pattern name + * @param {string} [options.index] - Custom index name + * @param {Object} [options.languageConfig] - Custom language configuration + * @returns {Array} Array of test configurations + */ +export const generateQueryTestConfigurations = ( + generateTestConfigurationCallback, + options = {} +) => { + const { + indexPattern = INDEX_PATTERN_WITH_TIME_1, + languageConfig = LanguageConfigs.DQL_Lucene, + } = options; + + const datasetTypes = createQueryDatasetTypes(languageConfig); + + return Object.values(datasetTypes).flatMap((dataset) => + dataset.supportedLanguages.map((language) => { + let datasetToUse; + switch (dataset.name) { + case datasetTypes.INDEX_PATTERN.name: + datasetToUse = indexPattern; + break; + default: + throw new Error( + `generateQueryTestConfigurations encountered unsupported dataset: ${dataset.name}` + ); + } + return generateTestConfigurationCallback(datasetToUse, dataset.name, language); + }) + ); +}; diff --git a/cypress/utils/apps/query_enhancements/shared.js b/cypress/utils/apps/query_enhancements/shared.js index a1766e788847..87bd503657eb 100644 --- a/cypress/utils/apps/query_enhancements/shared.js +++ b/cypress/utils/apps/query_enhancements/shared.js @@ -26,6 +26,26 @@ const getRandomString = () => Math.random().toString(36); export const getRandomizedWorkspaceName = () => `${WORKSPACE_NAME}-${getRandomString().substring(7)}`; +/** + * Generates base test configuration for tests + * @param {string} dataset - Dataset name + * @param {string} datasetType - Type of dataset + * @param {Object} language - Language configuration + * @returns {Object} Test configuration object + */ +export const generateBaseConfiguration = (dataset, datasetType, language) => { + const baseConfig = { + dataset, + datasetType, + language: language.name, + testName: `${language.name}-${datasetType}`, + }; + + return { + ...baseConfig, + }; +}; + /** * Callback for generateAllTestConfigurations * @callback GenerateTestConfigurationCallback diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index e3adadbe74cd..d3d5232741fd 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -61,3 +61,28 @@ Cypress.Commands.add('openWorkspaceDashboard', (workspaceName) => { .find('a.euiLink') .click(); }); + +Cypress.Commands.add('setAdvancedSetting', (changes) => { + const url = `${Cypress.config().baseUrl}/api/opensearch-dashboards/settings`; + + return cy + .request({ + method: 'POST', + url, + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + body: { changes }, // This is the key change - wrapping in changes object + failOnStatusCode: false, + }) + .then((response) => { + if (response.status === 400) { + throw new Error(`Bad request: ${response.body.message}`); + } + if (response.body.errors) { + console.error(response.body.items); + throw new Error('Setting advanced setting failed'); + } + }); +}); diff --git a/cypress/utils/commands.osd.js b/cypress/utils/commands.osd.js index 47149182c131..ed63ad606185 100644 --- a/cypress/utils/commands.osd.js +++ b/cypress/utils/commands.osd.js @@ -99,6 +99,9 @@ cy.osd.add('addDataSource', (options) => { const { name, url, authType = 'no_auth', credentials = {} } = options; + // in case the data source already exists, delete it first + cy.osd.deleteDataSourceByName(name); + // Visit the create data source page cy.visit('app/management/opensearch-dashboards/dataSources/create'); @@ -147,13 +150,32 @@ cy.osd.add('deleteDataSourceByName', (dataSourceName) => { // Navigate to the dataSource Management page cy.visit('app/dataSources'); + cy.get('h1').contains('Data sources').should('be.visible'); + cy.wait(2000); - // Find the anchor text corresponding to specified dataSource - cy.get('a').contains(dataSourceName).click(); + // Check if data source exists before trying to delete + cy.get('body').then(($body) => { + // First check if we're in empty state + const hasEmptyState = $body.find('[data-test-subj="datasourceTableEmptyState"]').length > 0; - // Delete the dataSource connection - cy.getElementByTestId('editDatasourceDeleteIcon').click(); - cy.getElementByTestId('confirmModalConfirmButton').click(); + if (hasEmptyState) { + cy.log(`No data sources exist - skipping deletion of ${dataSourceName}`); + return; + } + + // Then check if our specific data source exists + const dataSourceExists = $body.find(`a:contains("${dataSourceName}")`).length > 0; + + if (!dataSourceExists) { + cy.log(`Data source ${dataSourceName} not found - skipping deletion`); + return; + } + + // If we get here, the data source exists and we can delete it + cy.get('a').contains(dataSourceName).click(); + cy.getElementByTestId('editDatasourceDeleteIcon').should('be.visible').click(); + cy.getElementByTestId('confirmModalConfirmButton').should('be.visible').click(); + }); }); // Deletes all data sources. This command should only be used for convenience during development diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 27442c2e608b..8677d6104bc4 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -196,4 +196,5 @@ - [RELEASING](../RELEASING.md) - [SECURITY](../SECURITY.md) - [TESTING](../TESTING.md) + - [TRIAGING](../TRIAGING.md) - [TYPESCRIPT](../TYPESCRIPT.md) diff --git a/package.json b/package.json index ea655a408223..a7118c8325df 100644 --- a/package.json +++ b/package.json @@ -86,10 +86,12 @@ "release_note:generate": "scripts/use_node scripts/generate_release_note", "cypress:run-without-security": "env TZ=America/Los_Angeles NO_COLOR=1 cypress run --env SECURITY_ENABLED=false", "cypress:run-with-security": "env TZ=America/Los_Angeles NO_COLOR=1 cypress run --env SECURITY_ENABLED=true,openSearchUrl=https://localhost:9200,WAIT_FOR_LOADER_BUFFER_MS=500", - "osd:ciGroup10": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/saved_search.spec.js,$BASE_PATH/queries.spec.js,$BASE_PATH/a_check.spec.js,$BASE_PATH/dataset_selector.spec.js,$BASE_PATH/s3_dataset.spec.js,$BASE_PATH/simple_dataset_selector.spec.js\"", + "osd:ciGroup10": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/a_check.spec.js,$BASE_PATH/dataset_selector.spec.js,$BASE_PATH/queries.spec.js,$BASE_PATH/saved_search.spec.js,$BASE_PATH/simple_dataset_selector.spec.js\"", "osd:ciGroup11": "echo \"cypress/integration/dashboard_sanity_test.spec.ts\"", - "osd:ciGroup12": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/time_range_selection.spec.js,$BASE_PATH/saved_queries.spec.js,$BASE_PATH/language_specific_display.spec.js,$BASE_PATH/saved_search_in_dashboards.spec.js\"", - "osd:ciGroup13": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/sidebar.spec.js,$BASE_PATH/shared_links.spec.js,$BASE_PATH/table.spec.js,$BASE_PATH/field_display_filtering.spec.js,$BASE_PATH/inspect.spec.js,$BASE_PATH/caching.spec.js\"", + "osd:ciGroup12": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/language_specific_display.spec.js,$BASE_PATH/time_range_selection.spec.js,$BASE_PATH/saved_queries.spec.js,$BASE_PATH/saved_search_in_dashboards.spec.js\"", + "osd:ciGroup13": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/caching.spec.js,$BASE_PATH/field_display_filtering.spec.js,$BASE_PATH/inspect.spec.js,$BASE_PATH/shared_links.spec.js,$BASE_PATH/table.spec.js\"", + "osd:ciGroup14": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/s3_dataset.spec.js,$BASE_PATH/advanced_settings.spec.js,$BASE_PATH/queries_more.spec.js\"", + "osd:ciGroup15": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/autocomplete_switch.spec.js,$BASE_PATH/autocomplete_ui.spec.js,$BASE_PATH/autocomplete_query.spec.js,$BASE_PATH/sidebar.spec.js\"", "generate:opensearchsqlantlr": "./node_modules/antlr4ng-cli/index.js -Dlanguage=TypeScript -o ./src/plugins/data/public/antlr/opensearch_sql/.generated -visitor -no-listener -Xexact-output-dir ./src/plugins/data/public/antlr/opensearch_sql/grammar/OpenSearchSQLLexer.g4 ./src/plugins/data/public/antlr/opensearch_sql/grammar/OpenSearchSQLParser.g4", "generate:opensearchpplantlr": "./node_modules/antlr4ng-cli/index.js -Dlanguage=TypeScript -o ./src/plugins/data/public/antlr/opensearch_ppl/.generated -visitor -no-listener -Xexact-output-dir ./src/plugins/data/public/antlr/opensearch_ppl/grammar/OpenSearchPPLLexer.g4 ./src/plugins/data/public/antlr/opensearch_ppl/grammar/OpenSearchPPLParser.g4" }, diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index cc3b8e872c1e..521d3eb438cc 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -484,6 +484,7 @@ export { QueryControls, QueryResult, QueryStatus, + ResultStatus, SavedQuery, SavedQueryService, SavedQueryTimeFilter, diff --git a/src/plugins/data/public/query/query_string/index.ts b/src/plugins/data/public/query/query_string/index.ts index 9ec21a485663..e6546dfb9368 100644 --- a/src/plugins/data/public/query/query_string/index.ts +++ b/src/plugins/data/public/query/query_string/index.ts @@ -45,5 +45,6 @@ export { QueryControls, QueryResult, QueryStatus, + ResultStatus, LanguageReference, } from './language_service'; diff --git a/src/plugins/data/public/query/query_string/language_service/index.ts b/src/plugins/data/public/query/query_string/language_service/index.ts index 4030e3728fbd..49bf30478e50 100644 --- a/src/plugins/data/public/query/query_string/language_service/index.ts +++ b/src/plugins/data/public/query/query_string/language_service/index.ts @@ -10,5 +10,6 @@ export { QueryControls, QueryResult, QueryStatus, + ResultStatus, LanguageReference, } from './lib'; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap index 7588ef567b03..8c82e0dbe976 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap +++ b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap @@ -59,4 +59,19 @@ exports[`Query Result show error status with error message 2`] = ` `; -exports[`Query Result shows loading status 1`] = `""`; +exports[`Query Result shows loading status 1`] = ` + + + +`; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx index 2af3e81fc44f..8b82707807b6 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx @@ -58,8 +58,15 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { }; }, [props.queryStatus.startTime]); - if (elapsedTime > BUFFER_TIME && props.queryStatus.status === ResultStatus.LOADING) { + if (props.queryStatus.status === ResultStatus.LOADING) { const time = Math.floor(elapsedTime / 1000); + const loadingText = + elapsedTime > BUFFER_TIME + ? i18n.translate('data.query.languageService.queryResults.loadTime', { + defaultMessage: 'Loading {time} s', + values: { time }, + }) + : ''; return ( - {i18n.translate('data.query.languageService.queryResults.loadTime', { - defaultMessage: 'Loading {time} s', - values: { time }, - })} + + {loadingText} + ); } diff --git a/src/plugins/data/public/ui/query_editor/_query_editor.scss b/src/plugins/data/public/ui/query_editor/_query_editor.scss index d6577d33fdd3..257963defdab 100644 --- a/src/plugins/data/public/ui/query_editor/_query_editor.scss +++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss @@ -219,6 +219,10 @@ } } +.queryEditor__progress { + position: relative; +} + .queryEditor__footer { display: flex; gap: 4px; diff --git a/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss b/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss index 8a626dbe36e3..fbc33d555dc3 100644 --- a/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss +++ b/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss @@ -7,6 +7,10 @@ background-color: $euiColorLightestShade; } + &__progress { + position: relative; + } + .monaco-editor { border-radius: $euiSizeXS $euiSizeXS 0 0; diff --git a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx index 55b19894d41e..79e2ff8b9f85 100644 --- a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx +++ b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx @@ -4,8 +4,9 @@ */ import React from 'react'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiProgress } from '@elastic/eui'; import { monaco } from '@osd/monaco'; +import { QueryStatus, ResultStatus } from '../../../../query'; import { CodeEditor } from '../../../../../../opensearch_dashboards_react/public'; import { createEditor, SingleLineInput } from '../shared'; @@ -20,6 +21,7 @@ export interface DefaultInputProps extends React.JSX.IntrinsicAttributes { }; headerRef?: React.RefObject; provideCompletionItems: monaco.languages.CompletionItemProvider['provideCompletionItems']; + queryStatus?: QueryStatus; } export const DefaultInput: React.FC = ({ @@ -30,6 +32,7 @@ export const DefaultInput: React.FC = ({ editorDidMount, headerRef, provideCompletionItems, + queryStatus, }) => { return (
@@ -69,6 +72,11 @@ export const DefaultInput: React.FC = ({ }} triggerSuggestOnFocus={true} /> +
+ {queryStatus?.status === ResultStatus.LOADING && ( + + )} +
{footerItems && ( ['prepend']; footerItems?: any; + queryStatus?: QueryStatus; } type CollapsedComponent = React.ComponentType; @@ -63,6 +65,7 @@ export const SingleLineInput: React.FC = ({ provideCompletionItems, prepend, footerItems, + queryStatus, }) => { const [editorIsFocused, setEditorIsFocused] = useState(false); const blurTimeoutRef = useRef(); @@ -152,6 +155,11 @@ export const SingleLineInput: React.FC = ({ }} triggerSuggestOnFocus={true} /> +
+ {queryStatus?.status === ResultStatus.LOADING && ( + + )} +
{editorIsFocused && (
{footerItems && ( diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index 739814ff5611..c23c208f71df 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -341,6 +341,7 @@ export const QueryEditorUI: React.FC = (props) => { ], }, provideCompletionItems, + queryStatus: props.queryStatus, }; const singleLineInputProps = { @@ -401,6 +402,7 @@ export const QueryEditorUI: React.FC = (props) => { , ], }, + queryStatus: props.queryStatus, }; const languageEditorFunc = languageManager.getLanguage(query.language)!.editor; diff --git a/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts b/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts index f4ee80ecbe14..6cafbbde650e 100644 --- a/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts +++ b/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { timefilterServiceMock } from '../../../data/public/query/timefilter/timefilter_service.mock'; import { PPLQueryParser } from './ppl_parser'; +import { TimeCache } from './time_cache'; test('it should throw error if with invalid url object', () => { const searchApiMock = { @@ -11,7 +13,8 @@ test('it should throw error if with invalid url object', () => { toPromise: jest.fn(() => Promise.resolve({})), })), }; - const parser = new PPLQueryParser(searchApiMock); + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + const parser = new PPLQueryParser(timeCache, searchApiMock); expect(() => parser.parseUrl({}, {})).toThrowError(); expect(() => parser.parseUrl({}, { body: {} })).toThrowError(); expect(() => parser.parseUrl({}, { body: { query: {} } })).toThrowError(); @@ -23,13 +26,60 @@ test('it should parse url object', () => { toPromise: jest.fn(() => Promise.resolve({})), })), }; - const parser = new PPLQueryParser(searchApiMock); + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + const parser = new PPLQueryParser(timeCache, searchApiMock); const result = parser.parseUrl({}, { body: { query: 'source=test_index' } }); expect(result.dataObject).toEqual({}); expect(result.url).toEqual({ body: { query: 'source=test_index' } }); }); -it('should populate data to request', async () => { +test('it should parse url object with %timefield% with injecting time filter to ppl query', () => { + const from = new Date('2024-10-07T05:03:22.548Z'); + const to = new Date('2025-01-08T05:03:30.981Z'); + jest + .spyOn(TimeCache.prototype, 'getTimeBounds') + .mockReturnValue({ max: from.valueOf(), min: to.valueOf() }); + + const searchApiMock = { + search: jest.fn(() => ({ + toPromise: jest.fn(() => Promise.resolve({})), + })), + }; + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + timeCache.setTimeRange({ + from: from.toISOString(), + to: to.toISOString(), + mode: 'absolute', + }); + + const parser = new PPLQueryParser(timeCache, searchApiMock); + const result1 = parser.parseUrl( + {}, + { body: { query: 'source=test_index' }, '%timefield%': 'timestamp' } + ); + expect(result1.url).toEqual({ + body: { + query: + "source=test_index | where `timestamp` >= '2025-01-08 05:03:30.981' and `timestamp` <= '2024-10-07 05:03:22.548'", + }, + }); + + const result2 = parser.parseUrl( + {}, + { + body: { query: 'source=test_index | stats count() as doc_count' }, + '%timefield%': 'timestamp', + } + ); + expect(result2.url).toEqual({ + body: { + query: + "source=test_index | where `timestamp` >= '2025-01-08 05:03:30.981' and `timestamp` <= '2024-10-07 05:03:22.548' | stats count() as doc_count", + }, + }); +}); + +test('it should populate data to request', async () => { const searchApiMock = { search: jest.fn(() => ({ toPromise: jest.fn(() => @@ -37,7 +87,8 @@ it('should populate data to request', async () => { ), })), }; - const parser = new PPLQueryParser(searchApiMock); + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + const parser = new PPLQueryParser(timeCache, searchApiMock); const request = { url: { body: { query: 'source=test_index' } }, dataObject: { diff --git a/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts b/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts index 8babcfc6e387..d39491b7fcbc 100644 --- a/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts @@ -4,8 +4,13 @@ */ import { i18n } from '@osd/i18n'; +import moment from 'moment'; + import { Data, UrlObject, PPLQueryRequest } from './types'; import { SearchAPI } from './search_api'; +import { TimeCache } from './time_cache'; + +const TIMEFIELD = '%timefield%'; const getRequestName = (request: PPLQueryRequest, index: number) => request.dataObject.name || @@ -15,13 +20,29 @@ const getRequestName = (request: PPLQueryRequest, index: number) => }); export class PPLQueryParser { - searchAPI: SearchAPI; - - constructor(searchAPI: SearchAPI) { + constructor(private readonly timeCache: TimeCache, private readonly searchAPI: SearchAPI) { this.searchAPI = searchAPI; } + injectTimeFilter(query: string, timefield: string) { + if (this.timeCache._timeRange) { + const [source, ...others] = query.split('|'); + const bounds = this.timeCache.getTimeBounds(); + const from = moment.utc(bounds.min).format('YYYY-MM-DD HH:mm:ss.SSS'); + const to = moment.utc(bounds.max).format('YYYY-MM-DD HH:mm:ss.SSS'); + const timeFilter = `where \`${timefield}\` >= '${from}' and \`${timefield}\` <= '${to}'`; + if (others.length > 0) { + return `${source.trim()} | ${timeFilter} | ${others.map((s) => s.trim()).join(' | ')}`; + } + return `${source.trim()} | ${timeFilter}`; + } + return query; + } + parseUrl(dataObject: Data, url: UrlObject) { + const timefield = url[TIMEFIELD]; + delete url[TIMEFIELD]; + // data.url.body.query must be defined if (!url.body || !url.body.query || typeof url.body.query !== 'string') { throw new Error( @@ -34,6 +55,11 @@ export class PPLQueryParser { ); } + if (timefield) { + const query = this.injectTimeFilter(url.body.query, timefield); + url.body.query = query; + } + return { dataObject, url }; } diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 349e771c65b1..becfa8f32b6e 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -656,7 +656,7 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never opensearch: new OpenSearchQueryParser(this.timeCache, this.searchAPI, this.filters, onWarn), emsfile: new EmsFileParser(serviceSettings), url: new UrlParser(onWarn), - ppl: new PPLQueryParser(this.searchAPI), + ppl: new PPLQueryParser(this.timeCache, this.searchAPI), }; } const pending: PendingType = {};