diff --git a/.github/workflows/query-insights-dashboards-e2e-workflow.yml b/.github/workflows/query-insights-dashboards-e2e-workflow.yml new file mode 100644 index 000000000..7dfe64786 --- /dev/null +++ b/.github/workflows/query-insights-dashboards-e2e-workflow.yml @@ -0,0 +1,24 @@ +name: Query Insights Dashboards Release tests workflow in Bundled OpenSearch Dashboards +on: + pull_request: + branches: [ '**' ] +jobs: + changes: + runs-on: ubuntu-latest + outputs: + tests: ${{ steps.filter.outputs.tests }} + steps: + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + tests: + - 'cypress/**/query-insights-dashboards/**' + + tests: + needs: changes + if: ${{ needs.changes.outputs.tests == 'true' }} + uses: ./.github/workflows/release-e2e-workflow-template.yml + with: + test-name: Query Insights Dashboards + test-command: env CYPRESS_NO_COMMAND_LOG=1 yarn cypress:run-with-security --browser chromium --spec 'cypress/integration/plugins/query-insights-dashboards/*' diff --git a/cypress/fixtures/plugins/query-insights-dashboards/sample_document.json b/cypress/fixtures/plugins/query-insights-dashboards/sample_document.json new file mode 100644 index 000000000..db4e65331 --- /dev/null +++ b/cypress/fixtures/plugins/query-insights-dashboards/sample_document.json @@ -0,0 +1,43 @@ +{ + "@timestamp": "2099-11-15T13:12:00", + "message": "this is document 0", + "user": { + "id": "user1", + "name": "John Doe", + "email": "john.doe@example.com", + "roles": ["admin", "editor"] + }, + "request": { + "method": "GET", + "url": "/api/v1/resource", + "status": 200, + "response_time_ms": 123, + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5..." + } + }, + "location": { + "ip": "192.168.1.1", + "city": "Seattle", + "region": "Washington", + "country": "US" + }, + "application": { + "name": "OpenSearch Dashboard", + "version": "2.8.0", + "environment": "production" + }, + "event": { + "id": "event123", + "type": "user_action", + "outcome": "success", + "reason": null + }, + "tags": ["login", "dashboard", "analytics"], + "metrics": { + "cpu_usage": 2.4, + "memory_usage": 512, + "disk_space_remaining": 1048576 + } +} diff --git a/cypress/integration/plugins/query-insights-dashboards/1_top_queries.cy.js b/cypress/integration/plugins/query-insights-dashboards/1_top_queries.cy.js new file mode 100644 index 000000000..200b73ccf --- /dev/null +++ b/cypress/integration/plugins/query-insights-dashboards/1_top_queries.cy.js @@ -0,0 +1,132 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import sampleDocument from '../../../fixtures/plugins/query-insights-dashboards/sample_document.json'; +import { QUERY_INSIGHTS_METRICS } from '../../../utils/constants'; + +// Name of the test index used in tests +const indexName = 'sample_index'; + +/** + Helper function to clean up the environment: + - Deletes the test index. + - Disables the top queries features. + */ +const clearAll = () => { + cy.deleteIndexByName(indexName); + cy.disableTopQueries(QUERY_INSIGHTS_METRICS.LATENCY); + cy.disableTopQueries(QUERY_INSIGHTS_METRICS.CPU); + cy.disableTopQueries(QUERY_INSIGHTS_METRICS.MEMORY); +}; + +describe('Query Insights Dashboard', () => { + // Setup before each test + beforeEach(() => { + clearAll(); + cy.createIndexByName(indexName, sampleDocument); + cy.enableTopQueries(QUERY_INSIGHTS_METRICS.LATENCY); + cy.enableTopQueries(QUERY_INSIGHTS_METRICS.CPU); + cy.enableTopQueries(QUERY_INSIGHTS_METRICS.MEMORY); + cy.searchOnIndex(indexName); + // wait for 1s to avoid same timestamp + cy.wait(1000); + cy.searchOnIndex(indexName); + cy.wait(1000); + cy.searchOnIndex(indexName); + // waiting for the query insights queue to drain + cy.wait(10000); + cy.navigateToOverview(); + }); + + /** + * Validate the main overview page loads correctly + */ + it('should display the main overview page', () => { + cy.get('.euiBasicTable').should('be.visible'); + cy.contains('Query insights - Top N queries'); + cy.url().should('include', '/queryInsights'); + + // should display the query table on the overview page + cy.get('.euiBasicTable').should('be.visible'); + cy.get('.euiTableHeaderCell').should('have.length.greaterThan', 0); + // should have top n queries displayed on the table + cy.get('.euiTableRow').should('have.length.greaterThan', 0); + }); + + /** + * Validate sorting by the "Timestamp" column works correctly + */ + it('should sort the table by the Timestamp column', () => { + // waiting for the query insights queue to drain + cy.wait(10000); + cy.navigateToOverview(); + // Click the Timestamp column header to sort + cy.get('.euiTableHeaderCell').contains('Timestamp').click(); + cy.get('.euiTableRow') + .first() + .invoke('text') + .then((firstRowAfterSort) => { + const firstTimestamp = firstRowAfterSort.trim(); + cy.get('.euiTableHeaderCell').contains('Timestamp').click(); + cy.get('.euiTableRow') + .first() + .invoke('text') + .then((firstRowAfterSecondSort) => { + expect(firstRowAfterSecondSort.trim()).to.not.equal(firstTimestamp); + }); + }); + }); + + /** + * Validate pagination works as expected + */ + it('should paginate the query table', () => { + for (let i = 0; i < 20; i++) { + cy.searchOnIndex(indexName); + } + // waiting for the query insights queue to drain + cy.wait(10000); + cy.reload(); + cy.get('.euiPagination').should('be.visible'); + cy.get('.euiPagination__item').contains('2').click(); + // Verify rows on the second page + cy.get('.euiTableRow').should('have.length.greaterThan', 0); + }); + + it('should switch between tabs', () => { + // Click Configuration tab + cy.getElementByText('.euiTab', 'Configuration').click({ force: true }); + cy.contains('Query insights - Configuration'); + cy.url().should('include', '/configuration'); + + // Click back to Query Insights tab + cy.getElementByText('.euiTab', 'Top N queries').click({ force: true }); + cy.url().should('include', '/queryInsights'); + }); + + it('should filter queries', () => { + cy.get('.euiFieldSearch').should('be.visible'); + cy.get('.euiFieldSearch').type('sample_index'); + // Add assertions for filtered results + cy.get('.euiTableRow').should('have.length.greaterThan', 0); + }); + + it('should clear the search input and reset results', () => { + cy.get('.euiFieldSearch').type('random_string'); + cy.get('.euiTableRow').should('have.length.greaterThan', 0); + cy.get('.euiFieldSearch').clear(); + cy.get('.euiTableRow').should('have.length.greaterThan', 0); // Validate reset + }); + + it('should display a message when no top queries are found', () => { + clearAll(); // disable top n queries + // waiting for the query insights queue to drain + cy.wait(10000); + cy.reload(); + cy.contains('No items found'); + }); + + after(() => clearAll()); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js index c6090f69c..c330e82ae 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -34,6 +34,7 @@ import '../utils/plugins/notifications-dashboards/commands'; import '../utils/plugins/dashboards-assistant/commands'; import '../utils/dashboards/console/commands'; import '../utils/dashboards/workspace-plugin/commands'; +import '../utils/plugins/query-insights-dashboards/commands'; import 'cypress-real-events'; diff --git a/cypress/utils/constants.js b/cypress/utils/constants.js index 6c2b85058..89109ad5f 100644 --- a/cypress/utils/constants.js +++ b/cypress/utils/constants.js @@ -17,3 +17,4 @@ export * from './plugins/notifications-dashboards/constants'; export * from './plugins/security-dashboards-plugin/constants'; export * from './plugins/ml-commons-dashboards/constants'; export * from './plugins/security-analytics-dashboards-plugin/constants'; +export * from './plugins/query-insights-dashboards/constants'; diff --git a/cypress/utils/plugins/query-insights-dashboards/commands.js b/cypress/utils/plugins/query-insights-dashboards/commands.js new file mode 100644 index 000000000..82402e907 --- /dev/null +++ b/cypress/utils/plugins/query-insights-dashboards/commands.js @@ -0,0 +1,189 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const { + QUERY_INSIGHTS_OVERVIEW_PATH, + QUERY_INSIGHTS_CONFIGURATION_PATH, +} = require('./constants'); + +// /** +// * Overwrites the default visit command to authenticate before visiting +// */ +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { +// // Add the basic auth header when security enabled in the Opensearch cluster +// if (Cypress.env('security_enabled')) { +// if (options) { +// options.auth = ADMIN_AUTH; +// } else { +// options = { auth: ADMIN_AUTH }; +// } +// // Add query parameters - select the default OpenSearch Dashboards tenant +// options.qs = { security_tenant: 'private' }; +// return originalFn(url, options); +// } else { +// return originalFn(url, options); +// } +// }); + +// /** +// * Overwrite request command to support authentication similar to visit. +// * The request function parameters can be url, or (method, url), or (method, url, body). +// */ +// Cypress.Commands.overwrite('request', (originalFn, ...args) => { +// const defaults = {}; +// // Add the basic authentication header when security enabled in the Opensearch cluster +// if (Cypress.env('SECURITY_ENABLED')) { +// defaults.auth = ADMIN_AUTH; +// } + +// let options = {}; +// if (typeof args[0] === 'object' && args[0] !== null) { +// options = { ...args[0] }; +// } else if (args.length === 1) { +// [options.url] = args; +// } else if (args.length === 2) { +// [options.method, options.url] = args; +// } else if (args.length === 3) { +// [options.method, options.url, options.body] = args; +// } + +// return originalFn({ ...defaults, ...options }); +// }); + +Cypress.Commands.add('getElementByText', (locator, text) => { + Cypress.log({ message: `Get element by text: ${text}` }); + return locator + ? cy.get(locator).filter(`:contains("${text}")`).should('be.visible') + : cy.contains(text).should('be.visible'); +}); + +// Cypress.Commands.add('login', () => { +// // much faster than log in through UI +// cy.request({ +// method: 'POST', +// url: `${BASE_PATH}/auth/login`, +// body: ADMIN_AUTH, +// headers: { +// 'osd-xsrf': true, +// }, +// }); +// }); + +Cypress.Commands.add('enableTopQueries', (metric) => { + cy.request({ + method: 'PUT', + url: `${Cypress.env('openSearchUrl')}/_cluster/settings`, + body: { + persistent: { + [`search.insights.top_queries.${metric}.enabled`]: true, + [`search.insights.top_queries.${metric}.window_size`]: '1m', + [`search.insights.top_queries.${metric}.top_n_size`]: 100, + }, + }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add('disableTopQueries', (metric) => { + cy.request({ + method: 'PUT', + url: `${Cypress.env('openSearchUrl')}/_cluster/settings`, + body: { + persistent: { + [`search.insights.top_queries.${metric}.enabled`]: false, + }, + }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add('enableGrouping', () => { + cy.request({ + method: 'PUT', + url: `${Cypress.env('openSearchUrl')}/_cluster/settings`, + body: { + persistent: { + 'search.insights.top_queries.latency.enabled': true, + 'search.insights.top_queries.cpu.enabled': true, + 'search.insights.top_queries.memory.enabled': true, + 'search.insights.top_queries.group_by': 'similarity', + 'search.insights.top_queries.max_groups_excluding_topn': 100, + 'search.insights.top_queries.grouping.attributes.field_name': true, + 'search.insights.top_queries.grouping.attributes.field_type': true, + 'search.insights.top_queries.latency.top_n_size': 5, + 'search.insights.top_queries.cpu.top_n_size': 5, + 'search.insights.top_queries.memory.top_n_size': 5, + 'search.insights.top_queries.latency.window_size': '1m', + 'search.insights.top_queries.cpu.window_size': '1m', + 'search.insights.top_queries.memory.window_size': '1m', + }, + }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add('disableGrouping', () => { + cy.request({ + method: 'PUT', + url: `${Cypress.env('openSearchUrl')}/_cluster/settings`, + body: { + persistent: { + 'search.insights.top_queries.latency.enabled': false, + 'search.insights.top_queries.cpu.enabled': false, + 'search.insights.top_queries.memory.enabled': false, + 'search.insights.top_queries.group_by': 'none', + }, + }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add('createIndexByName', (indexName, body = {}) => { + cy.request('POST', `${Cypress.env('openSearchUrl')}/${indexName}/_doc`, body); +}); + +Cypress.Commands.add('searchOnIndex', (indexName, body = {}) => { + cy.request( + 'GET', + `${Cypress.env('openSearchUrl')}/${indexName}/_search`, + body + ); +}); + +Cypress.Commands.add('deleteIndexByName', (indexName) => { + cy.request({ + method: 'DELETE', + url: `${Cypress.env('openSearchUrl')}/${indexName}`, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add( + 'waitForPageLoad', + (fullUrl, { timeout = 60000, contains = null }) => { + Cypress.log({ + message: `Wait for url: ${fullUrl} to be loaded.`, + }); + cy.url({ timeout: timeout }) + .should('include', fullUrl) + .then(() => { + contains && cy.contains(contains).should('be.visible'); + }); + } +); + +Cypress.Commands.add('navigateToOverview', () => { + cy.visit(QUERY_INSIGHTS_OVERVIEW_PATH); + cy.waitForPageLoad(QUERY_INSIGHTS_OVERVIEW_PATH, { + contains: 'Query insights - Top N queries', + }); +}); + +Cypress.Commands.add('navigateToConfiguration', () => { + cy.visit(QUERY_INSIGHTS_CONFIGURATION_PATH); + cy.waitForPageLoad(QUERY_INSIGHTS_CONFIGURATION_PATH, { + contains: 'Query insights - Configuration', + }); +}); diff --git a/cypress/utils/plugins/query-insights-dashboards/constants.js b/cypress/utils/plugins/query-insights-dashboards/constants.js new file mode 100644 index 000000000..5ed0b8ff5 --- /dev/null +++ b/cypress/utils/plugins/query-insights-dashboards/constants.js @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../../base_constants'; + +export const QUERY_INSIGHTS_PLUGIN_NAME = 'query-insights-dashboards'; + +export const QUERY_INSIGHTS_OVERVIEW_PATH = `${BASE_PATH}/app/${QUERY_INSIGHTS_PLUGIN_NAME}#/queryInsights`; +export const QUERY_INSIGHTS_CONFIGURATION_PATH = `${BASE_PATH}/app/${QUERY_INSIGHTS_PLUGIN_NAME}#/configuration`; +export const QUERY_INSIGHTS_METRICS = { + LATENCY: 'latency', + CPU: 'cpu', + MEMORY: 'memory', +}; + +// export const ADMIN_AUTH = { +// username: Cypress.env('username'), +// password: Cypress.env('password'), +// }; diff --git a/site/index.html b/site/index.html index c87d9c9cb..3a1df527b 100644 --- a/site/index.html +++ b/site/index.html @@ -182,6 +182,9 @@

Plugins:

+ diff --git a/site/js/dashboard.js b/site/js/dashboard.js index b5411c2c8..4f0bba14b 100644 --- a/site/js/dashboard.js +++ b/site/js/dashboard.js @@ -175,6 +175,12 @@ const plugins = { ], }, }, + 'query-insights-dashboards': { + name: 'queryInsightsDashboards', + default: { + videos: ['1_top_queries.cy.js'], + }, + }, }; // eslint-disable-next-line no-unused-vars diff --git a/test_finder.sh b/test_finder.sh index 248c039f7..9683121c7 100755 --- a/test_finder.sh +++ b/test_finder.sh @@ -25,6 +25,7 @@ OSD_COMPONENT_TEST_MAP=( "OpenSearch-Dashboards:opensearch-dashboards" "mlCommonsDashboards:ml-commons-dashboards" "securityAnalyticsDashboards:security-analytics-dashboards-plugin" "assistantDashboards:dashboards-assistant" + "queryInsightsDashboards:query-insights-dashboards" "OpenSearch-Dashboards-ci-group-1:OpenSearch-Dashboards-ci-group-1" "OpenSearch-Dashboards-ci-group-2:OpenSearch-Dashboards-ci-group-2" "OpenSearch-Dashboards-ci-group-3:OpenSearch-Dashboards-ci-group-3"