diff --git a/common/utils/QueryUtils.ts b/common/utils/QueryUtils.ts index f84c2d63..bb7c5f1a 100644 --- a/common/utils/QueryUtils.ts +++ b/common/utils/QueryUtils.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SearchQueryRecord } from '../../types/types'; +import { SearchQueryRecord, LiveSearchQueryResponse } from '../../types/types'; +import { API_ENDPOINTS } from './apiendpoints'; // Utility function to fetch query by id and time range export const retrieveQueryById = async ( @@ -11,8 +12,7 @@ export const retrieveQueryById = async ( dataSourceId: string, start: string | null, end: string | null, - id: string | null, - verbose: boolean + id: string | null ): Promise => { const nullResponse = { response: { top_queries: [] } }; const params = { @@ -21,7 +21,6 @@ export const retrieveQueryById = async ( from: start, to: end, id, - verbose, }, }; @@ -63,3 +62,32 @@ export const retrieveQueryById = async ( return null; } }; + +export const retrieveLiveQueries = async (core: { + http: { get: (endpoint: string) => Promise }; +}): Promise => { + const nullResponse: LiveSearchQueryResponse = { + ok: true, + response: { live_queries: [] }, + }; + + const errorResponse: LiveSearchQueryResponse = { + ok: false, + response: { live_queries: [] }, + }; + + try { + const response: LiveSearchQueryResponse = await core.http.get(API_ENDPOINTS.LIVE_QUERIES); + const liveQueries = response?.response?.live_queries; + + if (Array.isArray(liveQueries)) { + return response; + } else { + console.warn('No live queries found in response'); + return nullResponse; + } + } catch (error) { + console.error('Error retrieving live queries:', error); + return errorResponse; + } +}; diff --git a/common/utils/apiendpoints.ts b/common/utils/apiendpoints.ts new file mode 100644 index 00000000..ecc1b4cd --- /dev/null +++ b/common/utils/apiendpoints.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const API_ENDPOINTS = { + LIVE_QUERIES: '/api/live_queries', + CANCEL_TASK: (taskId: string) => `/api/tasks/${taskId}/cancel`, +}; diff --git a/cypress/e2e/3_configurations.cy.js b/cypress/e2e/3_configurations.cy.js index c3c01ea2..e2be6a62 100644 --- a/cypress/e2e/3_configurations.cy.js +++ b/cypress/e2e/3_configurations.cy.js @@ -32,8 +32,9 @@ describe('Query Insights Configurations Page', () => { cy.get('h1').contains('Query insights - Configuration').should('be.visible'); // Validate the tabs cy.get('.euiTabs').should('be.visible'); - cy.get('.euiTab').should('have.length', 2); // Two tabs: 'Top N queries' and 'Configuration' + cy.get('.euiTab').should('have.length', 3); // Three tabs: 'Top N queries', 'Live queries' and 'Configuration' cy.contains('button', 'Top N queries').should('be.visible'); + cy.contains('button', 'Live queries').should('be.visible'); cy.contains('button', 'Configuration').should('have.class', 'euiTab-isSelected'); // Validate the panels // 6 panels: Settings, Status, Group Settings, Group Status, Delete After Settings, Delete After Status diff --git a/cypress/e2e/5_live_queries.cy.js b/cypress/e2e/5_live_queries.cy.js new file mode 100644 index 00000000..08e3f520 --- /dev/null +++ b/cypress/e2e/5_live_queries.cy.js @@ -0,0 +1,459 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('Inflight Queries Dashboard', () => { + beforeEach(() => { + cy.fixture('stub_live_queries.json').then((stubResponse) => { + cy.intercept('GET', '**/api/live_queries', { + statusCode: 200, + body: stubResponse, + }).as('getLiveQueries'); + }); + + cy.navigateToLiveQueries(); + cy.wait(1000); + cy.wait('@getLiveQueries'); + }); + + it('displays the correct page title', () => { + cy.contains('Query insights - In-flight queries scoreboard').should('be.visible'); + }); + + it('displays metrics panels correctly', () => { + cy.get('[data-test-subj="panel-active-queries"]').within(() => { + cy.contains('Active queries'); + cy.get('h2 > b').should('contain.text', '20'); + }); + + cy.get('[data-test-subj="panel-avg-elapsed-time"]').within(() => { + cy.contains('Avg. elapsed time'); + cy.get('h2 > b').should('contain.text', '7.19 s'); + cy.contains('(Avg. across 20)'); + }); + + cy.get('[data-test-subj="panel-longest-query"]').within(() => { + cy.contains('Longest running query'); + cy.get('h2 > b').should('contain.text', '9.69 s'); + cy.contains('ID: node-A1B2C4E5:3614'); + }); + + cy.get('[data-test-subj="panel-total-cpu"]').within(() => { + cy.contains('Total CPU usage'); + cy.get('h2 > b').should('contain.text', '1.68 ms'); + cy.contains('(Sum across 20)'); + }); + + cy.get('[data-test-subj="panel-total-memory"]').within(() => { + cy.contains('Total memory usage'); + cy.get('h2 > b').should('contain.text', '69.12 KB'); + cy.contains('(Sum across 20)'); + }); + }); + + it('verifies table headers and row content in memory table', () => { + const expectedHeaders = [ + '', + 'Timestamp', + 'Task ID', + 'Index', + 'Coordinator node', + 'Time elapsed', + 'CPU usage', + 'Memory usage', + 'Search type', + 'Status', + 'Actions', + ]; + + const expectedRow = [ + 'Jun 05, 2025 @ 10:24:26 PM', + 'node-A1B2C3D4E5:3600', + 'top_queries-2025.06.06-11009', + 'node-A1B2C3D4E5', + '7.99 s', + '89.95 µs', + '3.73 KB', + 'QUERY_THEN_FETCH', + 'Cancelled', + '', + ]; + + cy.get('.euiTable thead tr th').should(($headers) => { + const actualHeaders = [...$headers].map((el) => el.innerText.trim()); + expect(actualHeaders.length).to.eq(expectedHeaders.length); + expectedHeaders.forEach((expected, index) => { + expect(actualHeaders[index]).to.eq(expected); + }); + }); + + cy.get('.euiTable tbody tr') + .first() + .find('td') + .should(($cells) => { + const actualCells = [...$cells].map((el) => el.innerText.trim()); + const visibleData = actualCells.slice(1, expectedRow.length + 1); + + expectedRow.forEach((expected, index) => { + const valueToCheck = visibleData[index]; + expect(valueToCheck).to.eq(expected); + }); + }); + }); + + it('navigates to next page in table pagination', () => { + cy.wait('@getLiveQueries'); + cy.get('.euiPagination').should('be.visible'); + cy.get('.euiPagination__item').contains('2').click(); + cy.get('tbody tr').should('exist'); + }); + + it('selects all checkboxes and shows bulk cancel text', () => { + cy.get('.euiTable thead tr th input[type="checkbox"]').check({ force: true }); + cy.get('.euiTable tbody tr input[type="checkbox"]:checked').then(($rows) => { + const selectedCount = $rows.length; + const expectedText = `Cancel ${selectedCount} queries`; + + cy.contains(expectedText).should('be.visible'); + }); + }); + + it('disables auto-refresh when toggled off', () => { + cy.get('[data-test-subj="live-queries-autorefresh-toggle"]').as('toggle'); + cy.get('[data-test-subj="live-queries-refresh-interval"]').as('dropdown'); + + cy.get('@toggle').click(); + cy.get('@toggle').should('have.attr', 'aria-checked', 'false'); + cy.get('@dropdown').should('be.disabled'); + }); + + it('has expected refresh interval options', () => { + cy.get('[data-test-subj="live-queries-refresh-interval"] option').should(($options) => { + const values = [...$options].map((opt) => opt.innerText.trim()); + expect(values).to.include.members(['5 seconds', '10 seconds', '30 seconds', '1 minute']); + }); + }); + + it('manually refreshes data', () => { + cy.get('[data-test-subj="live-queries-refresh-button"]').click(); + cy.wait('@getLiveQueries'); + }); + + it('updates data periodically', () => { + cy.fixture('stub_live_queries.json').then((initialData) => { + let callCount = 0; + cy.intercept('GET', '**/api/live_queries', (req) => { + callCount++; + const modifiedData = { + ...initialData, + response: { + ...initialData.response, + live_queries: initialData.response.live_queries.map((query) => ({ + ...query, + id: `query${callCount}_${query.id}`, + })), + }, + }; + req.reply(modifiedData); + }).as('getPeriodicQueries'); + }); + + cy.navigateToLiveQueries(); + + cy.wait('@getPeriodicQueries'); + cy.wait('@getPeriodicQueries'); + cy.wait('@getPeriodicQueries'); + + cy.get('@getPeriodicQueries.all').should('have.length.at.least', 3); + }); + + it('displays correct chart data for node and index charts', () => { + return cy + .get('[data-test-subj="vega-chart-node"]') + .invoke('attr', 'data-chart-values') + .then((json) => { + const data = JSON.parse(json); + expect(data).to.deep.equal([ + { label: 'node-A1B2C3D4E5', value: 2 }, + { label: '4W2VTHIgQY-oB7dSrYz4B', value: 1 }, + { label: 'node-X9Y8Z7W6', value: 1 }, + { label: 'node-P0Q9R8S7', value: 1 }, + { label: '4W2VTHIgQY-oB7dSrYz4', value: 1 }, + { label: 'node-M2O3P4Q5', value: 1 }, + { label: 'node-B2C3D4E5', value: 1 }, + { label: 'node-N2O3P4Q5', value: 1 }, + { label: '2VTHIgQY-oB7dSrYz4BQ', value: 1 }, + { label: 'others', value: 10 }, + ]); + expect(data).to.have.length(10); + }) + .then(() => { + return cy + .get('[data-test-subj="vega-chart-index"]') + .invoke('attr', 'data-chart-values') + .then((json) => { + const data = JSON.parse(json); + expect(data).to.deep.include({ label: 'top_queries-2025.06.06-11009', value: 19 }); + expect(data).to.deep.include({ label: 'opensearch', value: 1 }); + expect(data).to.have.length(2); + }); + }); + }); + + it('handles empty response state', () => { + cy.intercept('GET', '**/api/live_queries', (req) => { + req.reply({ + statusCode: 200, + body: { + ok: true, + response: { + live_queries: [], + }, + }, + }); + }).as('getEmptyQueries'); + + cy.navigateToLiveQueries(); + cy.wait('@getEmptyQueries'); + cy.get('[data-test-subj="panel-active-queries"]').within(() => { + cy.contains('Active queries'); + cy.get('h2 > b').should('contain.text', '0'); + }); + + cy.get('[data-test-subj="panel-avg-elapsed-time"]').within(() => { + cy.contains('Avg. elapsed time'); + cy.get('h2 > b').should('contain.text', '0'); + }); + + cy.get('[data-test-subj="panel-longest-query"]').within(() => { + cy.contains('Longest running query'); + cy.get('h2 > b').should('contain.text', '0'); + }); + + cy.get('[data-test-subj="panel-total-cpu"]').within(() => { + cy.contains('Total CPU usage'); + cy.get('h2 > b').should('contain.text', '0'); + }); + + cy.get('[data-test-subj="panel-total-memory"]').within(() => { + cy.contains('Total memory usage'); + cy.get('h2 > b').should('contain.text', '0'); + }); + + cy.get('[data-test-subj="vega-chart-node"]').should('not.exist'); + cy.contains('p', 'Queries by Node') + .closest('.euiPanel') + .within(() => { + cy.contains('No data available').should('be.visible'); + }); + + cy.get('[data-test-subj="vega-chart-index"]').should('not.exist'); + cy.contains('p', 'Queries by Index') + .closest('.euiPanel') + .within(() => { + cy.contains('No data available').should('be.visible'); + }); + }); + it('validates time unit conversions', () => { + cy.intercept('GET', '**/api/live_queries', { + statusCode: 200, + body: { + response: { + live_queries: [ + { + measurements: { + latency: { number: 500 }, + cpu: { number: 100 }, + memory: { number: 1000 }, + }, + }, + ], + }, + }, + }).as('getMicrosecondsData'); + + cy.wait('@getMicrosecondsData'); + cy.get('.euiPanel') + .eq(1) + .within(() => { + cy.get('h2').contains(/0\.50\s*µs/); + }); + + cy.intercept('GET', '**/api/live_queries', { + statusCode: 200, + body: { + response: { + live_queries: [ + { + measurements: { + latency: { number: 1000000 }, + cpu: { number: 100000 }, + memory: { number: 1000 }, + }, + }, + ], + }, + }, + }).as('getMillisecondsData'); + + cy.wait('@getMillisecondsData'); + cy.get('.euiPanel') + .eq(1) + .within(() => { + cy.get('h2').contains(/1\.00\s*ms/); + }); + }); + + it('validates memory unit conversions', () => { + cy.intercept('GET', '**/api/live_queries', { + statusCode: 200, + body: { + response: { + live_queries: [ + { + timestamp: Date.now(), + id: 'kb-test', + node_id: 'n1', + description: 'test', + measurements: { + latency: { number: 1 }, + cpu: { number: 1 }, + memory: { number: 2048 }, + }, + }, + ], + }, + }, + }).as('getKBData'); + + cy.visit('/app/query-insights-dashboards#/LiveQueries'); + cy.wait('@getKBData'); + cy.contains('h2', /2\s*KB/).should('exist'); + + cy.intercept('GET', '**/api/live_queries', { + statusCode: 200, + body: { + response: { + live_queries: [ + { + timestamp: Date.now(), + id: 'mb-test', + node_id: 'n1', + description: 'test', + measurements: { + latency: { number: 1 }, + cpu: { number: 1 }, + memory: { number: 2 * 1024 * 1024 }, + }, + }, + ], + }, + }, + }).as('getMBData'); + + cy.get('[data-test-subj="live-queries-refresh-button"]').click(); + cy.wait('@getMBData'); + cy.contains('h2', /2\s*MB/).should('exist'); + }); + + it('does not show cancel action for already cancelled queries', () => { + cy.fixture('stub_live_queries.json').then((data) => { + data.response.live_queries[0].measurements.is_cancelled = true; + + cy.intercept('GET', '**/api/live_queries', { + statusCode: 200, + body: data, + }).as('getCancelledQuery'); + + cy.navigateToLiveQueries(); + cy.wait('@getCancelledQuery'); + + cy.contains(data.response.live_queries[0].id) + .parents('tr') + .within(() => { + cy.get('[aria-label="Cancel this query"]').should('not.exist'); + }); + + cy.contains(data.response.live_queries[0].id) + .parents('tr') + .find('input[type="checkbox"]') + .should('be.disabled'); + }); + }); + + it('filters table to show only "opensearch" index queries', () => { + cy.contains('button', 'Index').click(); + cy.contains('[role="option"]', 'opensearch').click(); + cy.get('tbody tr').should('have.length', 1); + cy.get('tbody tr') + .first() + .within(() => { + cy.contains('td', 'opensearch'); + }); + }); + + it('renders charts and allows switching between chart types', () => { + cy.contains('p', 'Queries by Node').closest('.euiPanel').as('nodeChart'); + cy.contains('p', 'Queries by Index').closest('.euiPanel').as('indexChart'); + + cy.get('@nodeChart').within(() => { + cy.get('.euiButtonGroup').should('exist'); + cy.get('.euiButtonGroup').contains('Donut').should('exist'); + cy.get('.euiButtonGroup').contains('Bar').should('exist'); + cy.get('.vega-embed').should('exist'); + + cy.get('.euiButtonGroup').contains('Bar').click(); + cy.wait(500); + cy.get('.euiButtonGroup').contains('Donut').click(); + cy.wait(500); + }); + + cy.get('@indexChart').within(() => { + cy.get('.euiButtonGroup').should('exist'); + cy.get('.euiButtonGroup').contains('Donut').should('exist'); + cy.get('.euiButtonGroup').contains('Bar').should('exist'); + cy.get('.vega-embed').should('exist'); + + cy.get('.euiButtonGroup').contains('Bar').click({ force: true }); + cy.wait(500); + cy.get('.euiButtonGroup').contains('Donut').click({ force: true }); + cy.wait(500); + }); + }); + + it('displays "others" in Vega chart legend when there are > 9 nodes', () => { + cy.get('[data-test-subj="vega-chart-node"] svg') + .should('exist') + .contains(/others/i) + .should('exist'); + }); + + it('displays error panel when live queries API fails', () => { + cy.intercept('GET', '**/api/live_queries', { + statusCode: 500, + body: { + ok: false, + error: 'Internal Server Error', + }, + }).as('getLiveQueriesError'); + + cy.visit('/app/query-insights-dashboards#/LiveQueries'); + cy.wait('@getLiveQueriesError'); + + // Validate that a proper error message is shown in the Vega panels + cy.contains('p', 'Queries by Node') + .closest('.euiPanel') + .within(() => { + cy.contains('Failed to load live queries').should('be.visible'); + }); + + cy.contains('p', 'Queries by Index') + .closest('.euiPanel') + .within(() => { + cy.contains('Failed to load live queries').should('be.visible'); + }); + cy.get('[data-test-subj="vega-chart-node"]').should('not.exist'); + cy.get('[data-test-subj="vega-chart-index"]').should('not.exist'); + }); +}); diff --git a/cypress/fixtures/stub_live_queries.json b/cypress/fixtures/stub_live_queries.json new file mode 100644 index 00000000..96b441c3 --- /dev/null +++ b/cypress/fixtures/stub_live_queries.json @@ -0,0 +1,487 @@ +{ + "ok": true, + "response": { + "live_queries": [ + { + "timestamp": 1749187466964, + "id": "node-A1B2C3D4E5:3600", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-A1B2C3D4E5", + "measurements": { + "latency": { + "number": 7990852130, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 89951, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3818, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187466969, + "id": "4W2VTHIgQY-oB7dSrYz4B:3601", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "4W2VTHIgQY-oB7dSrYz4B", + "measurements": { + "latency": { + "number": 6412938258, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 113698, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 2653, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187466974, + "id": "node-X9Y8Z7W6:3602", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-X9Y8Z7W6", + "measurements": { + "latency": { + "number": 9633985711, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 65783, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3996, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187466979, + "id": "node-P0Q9R8S7:3603", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-P0Q9R8S7", + "measurements": { + "latency": { + "number": 9530744641, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 84327, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3806, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187466984, + "id": "4W2VTHIgQY-oB7dSrYz4:3604", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "4W2VTHIgQY-oB7dSrYz4", + "measurements": { + "latency": { + "number": 6084339201, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 99929, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 4364, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187466989, + "id": "node-M2O3P4Q5:3605", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-M2O3P4Q5", + "measurements": { + "latency": { + "number": 5967857684, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 102069, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3952, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187466994, + "id": "node-B2C3D4E5:3606", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-B2C3D4E5", + "measurements": { + "latency": { + "number": 6454136095, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 86459, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3909, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187466999, + "id": "node-N2O3P4Q5:3607", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-N2O3P4Q5", + "measurements": { + "latency": { + "number": 8450990820, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 70640, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 2119, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187467004, + "id": "2VTHIgQY-oB7dSrYz4BQ:3608", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "2VTHIgQY-oB7dSrYz4BQ", + "measurements": { + "latency": { + "number": 7308762773, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 68275, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 4354, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187467009, + "id": "node-X9Y8Z7W6V5:3609", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-X9Y8Z7W6V5", + "measurements": { + "latency": { + "number": 5167823987, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 113606, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 2772, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187467014, + "id": "de-M1N2O3P4Q5:3610", + "description": "indices[opensearch], search_type[Query_THEN_FETCH]", + "node_id": "de-M1N2O3P4Q5", + "measurements": { + "latency": { + "number": 5708789476, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 76950, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 4238, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187467019, + "id": "no-P0Q9R8S7T6:3611", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "no-P0Q9R8S7T6", + "measurements": { + "latency": { + "number": 7501207171, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 115548, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3146, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187467024, + "id": "4WTHIgQY-oB7dSrYz4BQ:3612", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "4WTHIgQY-oB7dSrYz4BQ", + "measurements": { + "latency": { + "number": 8887346819, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 41212, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 2430, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187467029, + "id": "4W2VTHQY-oB7dSrYz4BQ:3613", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "4W2VTHQY-oB7dSrYz4BQ", + "measurements": { + "latency": { + "number": 5672726176, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 104855, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3759, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187467034, + "id": "node-A1B2C4E5:3614", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-A1B2C4E5", + "measurements": { + "latency": { + "number": 9691523794, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 57877, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3231, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187467039, + "id": "node-X8Z7W6V5:3615", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-X8Z7W6V5", + "measurements": { + "latency": { + "number": 6840833580, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 82656, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 3428, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": false + } + }, + { + "timestamp": 1749187467044, + "id": "node-M1N2P4Q5:3616", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-M1N2P4Q5", + "measurements": { + "latency": { + "number": 5718519414, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 105825, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 4775, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187467049, + "id": "node-A1B2C3E5:3617", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-A1B2C3E5", + "measurements": { + "latency": { + "number": 6063407743, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 54160, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 4814, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187467054, + "id": "4W2VTHIgQY-7dSrYz4BQ:3618", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "4W2VTHIgQY-7dSrYz4BQ", + "measurements": { + "latency": { + "number": 9215252980, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 65529, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 2855, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + }, + { + "timestamp": 1749187467059, + "id": "node-A1B2C3D4E5:3619", + "description": "indices[top_queries-2025.06.06-11009], search_type[QUERY_THEN_FETCH]", + "node_id": "node-A1B2C3D4E5", + "measurements": { + "latency": { + "number": 5409476701, + "count": 1, + "aggregationType": "NONE" + }, + "cpu": { + "number": 81905, + "count": 1, + "aggregationType": "NONE" + }, + "memory": { + "number": 2357, + "count": 1, + "aggregationType": "NONE" + }, + "is_cancelled": true + } + } + ] + } +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e05117c1..d1cfc7a5 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -3,7 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -const { ADMIN_AUTH, OVERVIEW_PATH, CONFIGURATION_PATH, BASE_PATH } = require('./constants'); +const { + ADMIN_AUTH, + OVERVIEW_PATH, + CONFIGURATION_PATH, + BASE_PATH, + LIVEQUERIES_PATH, +} = require('./constants'); /** * Overwrites the default visit command to authenticate before visiting @@ -209,6 +215,13 @@ Cypress.Commands.add('navigateToConfiguration', () => { cy.waitForPageLoad(CONFIGURATION_PATH, { contains: 'Query insights - Configuration' }); }); +Cypress.Commands.add('navigateToLiveQueries', () => { + cy.visit(LIVEQUERIES_PATH); + cy.waitForPageLoad(LIVEQUERIES_PATH, { + contains: 'Query insights - In-flight queries scoreboard', + }); +}); + Cypress.Commands.add('waitForPluginToLoad', () => { // CI environments need much longer waits for plugin initialization const isCI = Cypress.env('CI') || !Cypress.config('isInteractive'); diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 6b68b5b3..a5b0523c 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -9,6 +9,8 @@ export const PLUGIN_NAME = 'query-insights-dashboards'; export const OVERVIEW_PATH = `${BASE_PATH}/app/${PLUGIN_NAME}#/queryInsights`; export const CONFIGURATION_PATH = `${BASE_PATH}/app/${PLUGIN_NAME}#/configuration`; +export const LIVEQUERIES_PATH = `${BASE_PATH}/app/${PLUGIN_NAME}#/LiveQueries`; + export const METRICS = { LATENCY: 'latency', CPU: 'cpu', diff --git a/package.json b/package.json index a992369d..caa3a050 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,16 @@ "postbuild": "echo Renaming build artifact to [$npm_package_config_id-$npm_package_version.zip] && mv build/$npm_package_config_id*.zip build/$npm_package_config_id-$npm_package_version.zip" }, "dependencies": { - "object-hash": "^3.0.0", - "plotly.js-dist": "^2.34.0", "@babel/helpers": "^7.22.9", "@babel/runtime": "^7.26.10", - "@babel/runtime-corejs3": "^7.22.9" + "@babel/runtime-corejs3": "^7.22.9", + "filesize": "^10.1.6", + "object-hash": "^3.0.0", + "plotly.js-dist": "^2.34.0", + "vega": "^5.32.0", + "vega-embed": "6.21.0", + "vega-lite": "^5.6.0", + "luxon": "^3.2.1" }, "resolutions": { "@types/react": "^16.9.8", @@ -60,8 +65,9 @@ "@testing-library/dom": "^8.11.3", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^14.4.3", - "@types/react-dom": "^16.9.8", + "@types/luxon": "^3.6.2", "@types/object-hash": "^3.0.0", + "@types/react-dom": "^16.9.8", "@types/react-router-dom": "^5.3.2", "cypress": "^13.6.0", "cypress-real-events": "1.7.6", @@ -73,11 +79,11 @@ "jest-cli": "^27.5.1", "jest-environment-jsdom": "^27.5.1", "lint-staged": "^10.2.0", - "ts-loader": "^6.2.1", - "string.prototype.replaceall": "1.0.7" + "string.prototype.replaceall": "1.0.7", + "ts-loader": "^6.2.1" }, "eslintIgnore": [ "node_modules/*", "target/*" ] -} \ No newline at end of file +} diff --git a/public/components/__snapshots__/app.test.tsx.snap b/public/components/__snapshots__/app.test.tsx.snap index 60a61c8f..3ee67421 100644 --- a/public/components/__snapshots__/app.test.tsx.snap +++ b/public/components/__snapshots__/app.test.tsx.snap @@ -17,6 +17,18 @@ exports[` spec renders the component 1`] = ` class="euiTabs" role="tablist" > + + + Auto-refresh + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+

+ Active queries +

+
+

+ + 0 + +

+ +
+
+
+
+
+
+
+
+
+

+ Avg. elapsed time +

+
+

+ + 0 + +

+
+

+ (Avg. across + 0 + ) +

+
+
+
+
+
+
+
+
+
+
+

+ Longest running query +

+
+

+ + 0 + +

+
+
+
+
+
+
+
+
+
+

+ Total CPU usage +

+
+

+ + 0 + +

+
+

+ (Sum across + 0 + ) +

+
+
+
+
+
+
+
+
+
+
+

+ Total memory usage +

+
+

+ + 0 + +

+
+

+ (Sum across + 0 + ) +

+
+
+
+
+
+
+
+
+
+
+

+ Queries by Node +

+
+ + Chart Type + +
+ + +
+
+
+
+
+
+
+
+

+ No data available +

+
+
+
+
+
+
+
+
+
+

+ Queries by Index +

+
+ + Chart Type + +
+ + +
+
+
+
+
+
+
+
+

+ No data available +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ + + Timestamp + + + + + + Task ID + + + + + + Index + + + + + + Coordinator node + + + + + + Time elapsed + + + + + + CPU usage + + + + + + Memory usage + + + + + + Search type + + + + + + Status + + + + + + Actions + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+`; diff --git a/public/pages/TopNQueries/TopNQueries.test.tsx b/public/pages/TopNQueries/TopNQueries.test.tsx index 5c0aa5de..04b413cf 100644 --- a/public/pages/TopNQueries/TopNQueries.test.tsx +++ b/public/pages/TopNQueries/TopNQueries.test.tsx @@ -71,8 +71,9 @@ describe('TopNQueries Component', () => { // Check for Query Insights tab content expect(screen.getByText('Mocked QueryInsights')).toBeInTheDocument(); - expect(screen.getByText('Top N queries')).toBeInTheDocument(); - expect(screen.getByText('Configuration')).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Live queries' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Top N queries' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Configuration' })).toBeInTheDocument(); expect(container).toMatchSnapshot(); // Switch to Configuration tab diff --git a/public/pages/TopNQueries/TopNQueries.tsx b/public/pages/TopNQueries/TopNQueries.tsx index 630bca5f..1779b475 100644 --- a/public/pages/TopNQueries/TopNQueries.tsx +++ b/public/pages/TopNQueries/TopNQueries.tsx @@ -12,6 +12,7 @@ import { DataSourceOption } from 'src/plugins/data_source_management/public/comp import QueryInsights from '../QueryInsights/QueryInsights'; import Configuration from '../Configuration/Configuration'; import QueryDetails from '../QueryDetails/QueryDetails'; +import { InflightQueries } from '../InflightQueries/InflightQueries'; import { SearchQueryRecord } from '../../../types/types'; import { QueryGroupDetails } from '../QueryGroupDetails/QueryGroupDetails'; import { QueryInsightsDashboardsPluginStartDependencies } from '../../types'; @@ -38,6 +39,7 @@ import { getDataSourceFromUrl } from '../../utils/datasource-utils'; export const QUERY_INSIGHTS = '/queryInsights'; export const CONFIGURATION = '/configuration'; +export const LIVE_QUERIES = '/LiveQueries'; export interface MetricSettings { isEnabled: boolean; @@ -131,6 +133,11 @@ const TopNQueries = ({ const [queries, setQueries] = useState([]); const tabs: Array<{ id: string; name: string; route: string }> = [ + { + id: 'liveQueries', + name: 'Live queries', + route: LIVE_QUERIES, + }, { id: 'topNQueries', name: 'Top N queries', @@ -394,6 +401,28 @@ const TopNQueries = ({ ); }} + + + +

Query insights - In-flight queries scoreboard

+
+ + + } + /> + {tabs.map(renderTab)} + + +
+