From c58cc2bdcf04ac0a32c6502d0f6eb6a0c9aec038 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:50:41 -0800 Subject: [PATCH] MDS support for query insights dashboards (#71) (#72) * Add support for MDS * MDS for all paths, pages with dummy dataSourceId * Fix all pages and apis * Push data source into the url * Persist data source selection across pages * fix when local cluster id is empty * fix manage link based on OpenSearch-Dashboards/pull/9059 * refresh page data when data source is changed * fix lint * fix unit tests and cypress tests * make data source picker read only on detail pages --------- (cherry picked from commit e4ccdb802acdd8d2b1f4c2244dfa8f94005b4abe) Signed-off-by: Derek Ho Signed-off-by: David Zane Signed-off-by: Chenyang Ji Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: Derek Ho Co-authored-by: David Zane --- cypress/e2e/3_configurations.cy.js | 37 ++- opensearch_dashboards.json | 3 +- public/application.tsx | 15 +- public/components/DataSourcePicker.tsx | 88 ++++++ public/components/app.test.tsx | 39 ++- public/components/app.tsx | 20 +- .../Configuration/Configuration.test.tsx | 34 ++- public/pages/Configuration/Configuration.tsx | 41 ++- .../__snapshots__/Configuration.test.tsx.snap | 2 + .../pages/QueryDetails/QueryDetails.test.tsx | 26 +- public/pages/QueryDetails/QueryDetails.tsx | 38 ++- .../QueryGroupDetails.test.tsx | 27 +- .../QueryGroupDetails/QueryGroupDetails.tsx | 37 ++- .../QueryInsights/QueryInsights.test.tsx | 38 ++- public/pages/QueryInsights/QueryInsights.tsx | 237 ++++++++-------- public/pages/TopNQueries/TopNQueries.test.tsx | 28 +- public/pages/TopNQueries/TopNQueries.tsx | 191 ++++++++----- public/pages/Utils/QueryUtils.ts | 2 + public/plugin.ts | 9 +- public/types.ts | 10 +- server/plugin.ts | 15 +- server/routes/index.ts | 253 ++++++++++++------ 22 files changed, 852 insertions(+), 338 deletions(-) create mode 100644 public/components/DataSourcePicker.tsx diff --git a/cypress/e2e/3_configurations.cy.js b/cypress/e2e/3_configurations.cy.js index fe705b8..ac8fc58 100644 --- a/cypress/e2e/3_configurations.cy.js +++ b/cypress/e2e/3_configurations.cy.js @@ -11,6 +11,13 @@ const clearAll = () => { cy.disableTopQueries(METRICS.MEMORY); }; +const toggleMetricEnabled = async () => { + cy.get('button[data-test-subj="top-n-metric-toggle"]').trigger('mouseover'); + cy.wait(1000); + cy.get('button[data-test-subj="top-n-metric-toggle"]').click({ force: true }); + cy.wait(1000); +}; + describe('Query Insights Configurations Page', () => { beforeEach(() => { clearAll(); @@ -66,22 +73,30 @@ describe('Query Insights Configurations Page', () => { */ it('should allow enabling and disabling metrics', () => { // Validate the switch for enabling/disabling metrics - cy.get('button[role="switch"]').should('exist'); - // Toggle the switch - cy.get('button[role="switch"]') - .first() - .should('have.attr', 'aria-checked', 'false') // Initially disabled - .click() - .should('have.attr', 'aria-checked', 'true'); // After toggling, it should be enabled + cy.get('button[data-test-subj="top-n-metric-toggle"]') + .should('exist') + .and('have.attr', 'aria-checked', 'false') // Initially disabled) + .trigger('mouseover') + .click(); + cy.wait(1000); + + cy.get('button[data-test-subj="top-n-metric-toggle"]').should( + 'have.attr', + 'aria-checked', + 'true' + ); // After toggling, it should be enabled // Re-enable the switch - cy.get('button[role="switch"]').first().click().should('have.attr', 'aria-checked', 'false'); + cy.get('button[data-test-subj="top-n-metric-toggle"]') + .trigger('mouseover') + .click() + .should('have.attr', 'aria-checked', 'false'); }); /** * Validate the value of N (count) input */ it('should allow updating the value of N (count)', () => { - cy.get('button[role="switch"]').first().click(); + toggleMetricEnabled(); // Locate the input for N cy.get('input[type="number"]').should('have.attr', 'value', '3'); // Default 3 // Change the value to 50 @@ -95,7 +110,7 @@ describe('Query Insights Configurations Page', () => { * Validate the window size dropdowns */ it('should allow selecting a window size and unit', () => { - cy.get('button[role="switch"]').first().click(); + toggleMetricEnabled(); // Validate default values cy.get('select#timeUnit').should('have.value', 'MINUTES'); // Default unit is "Minute(s)" // Test valid time unit selection @@ -192,7 +207,7 @@ describe('Query Insights Configurations Page', () => { * After saving the status panel should show the correct status */ it('should allow saving the configuration', () => { - cy.get('button[role="switch"]').first().click(); + toggleMetricEnabled(); cy.get('select#timeUnit').select('MINUTES'); cy.get('select#minutes').select('5'); cy.get('button[data-test-subj="save-config-button"]').click(); diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 7310231..4dd344a 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -5,5 +5,6 @@ "server": true, "ui": true, "requiredPlugins": ["navigation"], - "optionalPlugins": [] + "optionalPlugins": ["dataSource", + "dataSourceManagement"] } diff --git a/public/application.tsx b/public/application.tsx index 70850a8..8c6560e 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -9,18 +9,25 @@ import { HashRouter as Router } from 'react-router-dom'; import { AppMountParameters, CoreStart } from '../../../src/core/public'; import { QueryInsightsDashboardsApp } from './components/app'; import { QueryInsightsDashboardsPluginStartDependencies } from './types'; +import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; export const renderApp = ( core: CoreStart, depsStart: QueryInsightsDashboardsPluginStartDependencies, - { element }: AppMountParameters + params: AppMountParameters, + dataSourceManagement?: DataSourceManagementPluginSetup ) => { ReactDOM.render( - + , - element + params.element ); - return () => ReactDOM.unmountComponentAtNode(element); + return () => ReactDOM.unmountComponentAtNode(params.element); }; diff --git a/public/components/DataSourcePicker.tsx b/public/components/DataSourcePicker.tsx new file mode 100644 index 0000000..3621deb --- /dev/null +++ b/public/components/DataSourcePicker.tsx @@ -0,0 +1,88 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + DataSourceManagementPluginSetup, + DataSourceOption, + DataSourceSelectableConfig, +} from 'src/plugins/data_source_management/public'; +import { AppMountParameters, CoreStart } from '../../../../src/core/public'; +import { QueryInsightsDashboardsPluginStartDependencies } from '../types'; + +export interface DataSourceMenuProps { + dataSourceManagement: DataSourceManagementPluginSetup; + depsStart: QueryInsightsDashboardsPluginStartDependencies; + coreStart: CoreStart; + params: AppMountParameters; + setDataSource: React.Dispatch>; + selectedDataSource: DataSourceOption; + onManageDataSource: () => void; + onSelectedDataSource: () => void; + dataSourcePickerReadOnly: boolean; +} + +export function getDataSourceEnabledUrl(dataSource: DataSourceOption) { + const url = new URL(window.location.href); + url.searchParams.set('dataSource', JSON.stringify(dataSource)); + return url; +} + +export function getDataSourceFromUrl(): DataSourceOption { + const urlParams = new URLSearchParams(window.location.search); + const dataSourceParam = (urlParams && urlParams.get('dataSource')) || '{}'; + // following block is needed if the dataSource param is set to non-JSON value, say 'undefined' + try { + return JSON.parse(dataSourceParam); + } catch (e) { + return JSON.parse('{}'); // Return an empty object or some default value if parsing fails + } +} + +export const QueryInsightsDataSourceMenu = React.memo( + (props: DataSourceMenuProps) => { + const { + coreStart, + depsStart, + dataSourceManagement, + params, + setDataSource, + selectedDataSource, + onManageDataSource, + onSelectedDataSource, + dataSourcePickerReadOnly, + } = props; + const { setHeaderActionMenu } = params; + const DataSourceMenu = dataSourceManagement?.ui.getDataSourceMenu(); + + const dataSourceEnabled = !!depsStart.dataSource?.dataSourceEnabled; + + const wrapSetDataSourceWithUpdateUrl = (dataSources: DataSourceOption[]) => { + window.history.replaceState({}, '', getDataSourceEnabledUrl(dataSources[0]).toString()); + setDataSource(dataSources[0]); + onSelectedDataSource(); + }; + + return dataSourceEnabled ? ( + + ) : null; + }, + (prevProps, newProps) => + prevProps.selectedDataSource.id === newProps.selectedDataSource.id && + prevProps.dataSourcePickerReadOnly === newProps.dataSourcePickerReadOnly +); diff --git a/public/components/app.test.tsx b/public/components/app.test.tsx index eae7f7e..0ac8c47 100644 --- a/public/components/app.test.tsx +++ b/public/components/app.test.tsx @@ -8,28 +8,41 @@ import { render } from '@testing-library/react'; import { coreMock } from '../../../../src/core/public/mocks'; import { MemoryRouter as Router } from 'react-router-dom'; import { QueryInsightsDashboardsApp } from './app'; +import { createMemoryHistory } from 'history'; describe(' spec', () => { it('renders the component', () => { - const mockHttpStart = { - basePath: { - serverBasePath: '/app/opensearch-dashboards', + const coreStart = coreMock.createStart(); + // Mock AppMountParameters + const params = { + appBasePath: '/', + history: createMemoryHistory(), + setHeaderActionMenu: jest.fn(), + element: document.createElement('div'), + }; + // Mock plugin dependencies + const depsStart = { + navigation: { + ui: { TopNavMenu: () => null }, + }, + data: { + dataSources: { + dataSourceService: jest.fn(), + }, }, }; - const coreStart = coreMock.createStart(); - const { container } = render( null }, - } as any - } - notifications={{} as any} + depsStart={depsStart} + params={params} + dataSourceManagement={{ + ui: { + getDataSourceMenu: jest.fn(), + getDataSourceSelector: jest.fn(), + }, + }} /> ); diff --git a/public/components/app.tsx b/public/components/app.tsx index f526e4f..f8032cd 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -5,16 +5,32 @@ import React from 'react'; import { Route } from 'react-router-dom'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; import TopNQueries from '../pages/TopNQueries/TopNQueries'; -import { CoreStart } from '../../../../src/core/public'; +import { AppMountParameters, CoreStart } from '../../../../src/core/public'; import { QueryInsightsDashboardsPluginStartDependencies } from '../types'; export const QueryInsightsDashboardsApp = ({ core, depsStart, + params, + dataSourceManagement, }: { core: CoreStart; depsStart: QueryInsightsDashboardsPluginStartDependencies; + params: AppMountParameters; + dataSourceManagement?: DataSourceManagementPluginSetup; }) => { - return } />; + return ( + ( + + )} + /> + ); }; diff --git a/public/pages/Configuration/Configuration.test.tsx b/public/pages/Configuration/Configuration.test.tsx index b235b57..91680f1 100644 --- a/public/pages/Configuration/Configuration.test.tsx +++ b/public/pages/Configuration/Configuration.test.tsx @@ -8,6 +8,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router-dom'; import Configuration from './Configuration'; +import { DataSourceContext } from '../TopNQueries/TopNQueries'; const mockConfigInfo = jest.fn(); const mockCoreStart = { @@ -39,17 +40,34 @@ const groupBySettings = { groupBy: 'SIMILARITY', }; +const dataSourceMenuMock = jest.fn(() =>
Mock DataSourceMenu
); + +const dataSourceManagementMock = { + ui: { + getDataSourceMenu: jest.fn().mockReturnValue(dataSourceMenuMock), + }, +}; +const mockDataSourceContext = { + dataSource: { id: 'test', label: 'Test' }, + setDataSource: jest.fn(), +}; + const renderConfiguration = (overrides = {}) => render( - + + + ); diff --git a/public/pages/Configuration/Configuration.tsx b/public/pages/Configuration/Configuration.tsx index 47e099a..709a004 100644 --- a/public/pages/Configuration/Configuration.tsx +++ b/public/pages/Configuration/Configuration.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect, useContext } from 'react'; import { EuiBottomBar, EuiButton, @@ -23,14 +23,22 @@ import { EuiTitle, } from '@elastic/eui'; import { useHistory, useLocation } from 'react-router-dom'; -import { CoreStart } from 'opensearch-dashboards/public'; -import { QUERY_INSIGHTS, MetricSettings, GroupBySettings } from '../TopNQueries/TopNQueries'; +import { AppMountParameters, CoreStart } from 'opensearch-dashboards/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; +import { + QUERY_INSIGHTS, + MetricSettings, + GroupBySettings, + DataSourceContext, +} from '../TopNQueries/TopNQueries'; import { METRIC_TYPES_TEXT, TIME_UNITS_TEXT, MINUTES_OPTIONS, GROUP_BY_OPTIONS, } from '../Utils/Constants'; +import { QueryInsightsDataSourceMenu } from '../../components/DataSourcePicker'; +import { QueryInsightsDashboardsPluginStartDependencies } from '../../types'; const Configuration = ({ latencySettings, @@ -39,6 +47,9 @@ const Configuration = ({ groupBySettings, configInfo, core, + depsStart, + params, + dataSourceManagement, }: { latencySettings: MetricSettings; cpuSettings: MetricSettings; @@ -46,6 +57,9 @@ const Configuration = ({ groupBySettings: GroupBySettings; configInfo: any; core: CoreStart; + params: AppMountParameters; + dataSourceManagement?: DataSourceManagementPluginSetup; + depsStart: QueryInsightsDashboardsPluginStartDependencies; }) => { const history = useHistory(); const location = useLocation(); @@ -56,6 +70,7 @@ const Configuration = ({ const [windowSize, setWindowSize] = useState(latencySettings.currWindowSize); const [time, setTime] = useState(latencySettings.currTimeUnit); const [groupBy, setGroupBy] = useState(groupBySettings.groupBy); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; const [metricSettingsMap, setMetricSettingsMap] = useState({ latency: latencySettings, @@ -170,6 +185,19 @@ const Configuration = ({ return (
+ {}} + onSelectedDataSource={() => { + configInfo(true); + }} + dataSourcePickerReadOnly={false} + /> @@ -214,7 +242,12 @@ const Configuration = ({ - + diff --git a/public/pages/Configuration/__snapshots__/Configuration.test.tsx.snap b/public/pages/Configuration/__snapshots__/Configuration.test.tsx.snap index 091a526..23ad8c4 100644 --- a/public/pages/Configuration/__snapshots__/Configuration.test.tsx.snap +++ b/public/pages/Configuration/__snapshots__/Configuration.test.tsx.snap @@ -157,6 +157,7 @@ exports[`Configuration Component renders with default settings: should match def aria-checked="true" aria-labelledby="random_html_id" class="euiSwitch__button" + data-test-subj="top-n-metric-toggle" id="random_html_id" role="switch" type="button" @@ -970,6 +971,7 @@ exports[`Configuration Component updates state when toggling metrics and enables aria-checked="true" aria-labelledby="random_html_id" class="euiSwitch__button" + data-test-subj="top-n-metric-toggle" id="random_html_id" role="switch" type="button" diff --git a/public/pages/QueryDetails/QueryDetails.test.tsx b/public/pages/QueryDetails/QueryDetails.test.tsx index a545fca..3b51c6f 100644 --- a/public/pages/QueryDetails/QueryDetails.test.tsx +++ b/public/pages/QueryDetails/QueryDetails.test.tsx @@ -13,6 +13,7 @@ import plotly from 'plotly.js-dist'; import { MemoryRouter, Route } from 'react-router-dom'; import hash from 'object-hash'; import { retrieveQueryById } from '../Utils/QueryUtils'; +import { DataSourceContext } from '../TopNQueries/TopNQueries'; jest.mock('plotly.js-dist', () => ({ newPlot: jest.fn(), @@ -31,6 +32,18 @@ const mockCoreStart = { }, }; +const dataSourceMenuMock = jest.fn(() =>
Mock DataSourceMenu
); + +const dataSourceManagementMock = { + ui: { + getDataSourceMenu: jest.fn().mockReturnValue(dataSourceMenuMock), + }, +}; +const mockDataSourceContext = { + dataSource: { id: 'test', label: 'Test' }, + setDataSource: jest.fn(), +}; + const mockQuery = MockQueries()[0]; describe('QueryDetails component', () => { @@ -48,9 +61,16 @@ describe('QueryDetails component', () => { )}&from=2025-01-21T22:30:33.347Z&to=2025-01-22T22:30:33.347Z`, ]} > - - - + + + + + ); }; diff --git a/public/pages/QueryDetails/QueryDetails.tsx b/public/pages/QueryDetails/QueryDetails.tsx index 4ecb2d5..ee28bc1 100644 --- a/public/pages/QueryDetails/QueryDetails.tsx +++ b/public/pages/QueryDetails/QueryDetails.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; // @ts-ignore import Plotly from 'plotly.js-dist'; import { @@ -19,19 +19,28 @@ import { EuiTitle, } from '@elastic/eui'; import { useHistory, useLocation } from 'react-router-dom'; -import { CoreStart } from 'opensearch-dashboards/public'; +import { AppMountParameters, CoreStart } from 'opensearch-dashboards/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; import QuerySummary from './Components/QuerySummary'; -import { QUERY_INSIGHTS } from '../TopNQueries/TopNQueries'; +import { DataSourceContext, QUERY_INSIGHTS } from '../TopNQueries/TopNQueries'; import { SearchQueryRecord } from '../../../types/types'; import { PageHeader } from '../../components/PageHeader'; import { QueryInsightsDashboardsPluginStartDependencies } from '../../types'; import { retrieveQueryById } from '../Utils/QueryUtils'; +import { + getDataSourceFromUrl, + QueryInsightsDataSourceMenu, +} from '../../components/DataSourcePicker'; const QueryDetails = ({ core, depsStart, + params, + dataSourceManagement, }: { core: CoreStart; + params: AppMountParameters; + dataSourceManagement?: DataSourceManagementPluginSetup; depsStart: QueryInsightsDashboardsPluginStartDependencies; }) => { // Get url parameters @@ -43,6 +52,7 @@ const QueryDetails = ({ const [query, setQuery] = useState(null); const history = useHistory(); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; // Convert UNIX time to a readable format const convertTime = useCallback((unixTime: number) => { @@ -51,12 +61,12 @@ const QueryDetails = ({ return `${month} ${day}, ${year} @ ${date.toLocaleTimeString('en-US')}`; }, []); - useEffect(() => { - const fetchQueryDetails = async () => { - const retrievedQuery = await retrieveQueryById(core, from, to, id); - setQuery(retrievedQuery); - }; + const fetchQueryDetails = async () => { + const retrievedQuery = await retrieveQueryById(core, getDataSourceFromUrl().id, from, to, id); + setQuery(retrievedQuery); + }; + useEffect(() => { if (id && from && to) { fetchQueryDetails(); } @@ -134,7 +144,17 @@ const QueryDetails = ({ } /> - + {}} + onSelectedDataSource={fetchQueryDetails} + dataSourcePickerReadOnly={true} + /> diff --git a/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx index db49f37..9965f39 100644 --- a/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx +++ b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { mockQueries } from '../../../test/mocks/mockQueries'; import '@testing-library/jest-dom/extend-expect'; import { retrieveQueryById } from '../Utils/QueryUtils'; +import { DataSourceContext } from '../TopNQueries/TopNQueries'; jest.mock('object-hash', () => jest.fn(() => '8c1e50c035663459d567fa11d8eb494d')); @@ -29,6 +30,18 @@ jest.mock('react-ace', () => ({ default: () =>
Mocked Ace Editor
, })); +const dataSourceMenuMock = jest.fn(() =>
Mock DataSourceMenu
); + +const dataSourceManagementMock = { + ui: { + getDataSourceMenu: jest.fn().mockReturnValue(dataSourceMenuMock), + }, +}; +const mockDataSourceContext = { + dataSource: { id: 'test', label: 'Test' }, + setDataSource: jest.fn(), +}; + const mockQuery = mockQueries[0]; describe('QueryGroupDetails', () => { @@ -51,9 +64,16 @@ describe('QueryGroupDetails', () => { - - - + + + + + ); }; @@ -78,6 +98,7 @@ describe('QueryGroupDetails', () => { await waitFor(() => { expect(retrieveQueryById).toHaveBeenCalledWith( coreMock, + undefined, '1632441600000', '1632528000000', 'mockId' diff --git a/public/pages/QueryGroupDetails/QueryGroupDetails.tsx b/public/pages/QueryGroupDetails/QueryGroupDetails.tsx index f679815..6ec318b 100644 --- a/public/pages/QueryGroupDetails/QueryGroupDetails.tsx +++ b/public/pages/QueryGroupDetails/QueryGroupDetails.tsx @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CoreStart } from 'opensearch-dashboards/public'; +import { AppMountParameters, CoreStart } from 'opensearch-dashboards/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; // @ts-ignore import Plotly from 'plotly.js-dist'; import { useHistory, useLocation } from 'react-router-dom'; -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { EuiButton, EuiCodeBlock, @@ -21,7 +22,7 @@ import { EuiTitle, EuiIconTip, } from '@elastic/eui'; -import { QUERY_INSIGHTS } from '../TopNQueries/TopNQueries'; +import { DataSourceContext, QUERY_INSIGHTS } from '../TopNQueries/TopNQueries'; import { QueryGroupAggregateSummary } from './Components/QueryGroupAggregateSummary'; import { QueryGroupSampleQuerySummary } from './Components/QueryGroupSampleQuerySummary'; @@ -29,12 +30,20 @@ import { QueryInsightsDashboardsPluginStartDependencies } from '../../types'; import { PageHeader } from '../../components/PageHeader'; import { SearchQueryRecord } from '../../../types/types'; import { retrieveQueryById } from '../Utils/QueryUtils'; +import { + getDataSourceFromUrl, + QueryInsightsDataSourceMenu, +} from '../../components/DataSourcePicker'; export const QueryGroupDetails = ({ core, depsStart, + params, + dataSourceManagement, }: { core: CoreStart; + params: AppMountParameters; + dataSourceManagement?: DataSourceManagementPluginSetup; depsStart: QueryInsightsDashboardsPluginStartDependencies; }) => { // Get url parameters @@ -45,6 +54,7 @@ export const QueryGroupDetails = ({ const to = searchParams.get('to'); const [query, setQuery] = useState(null); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; const convertTime = (unixTime: number) => { const date = new Date(unixTime); @@ -54,12 +64,12 @@ export const QueryGroupDetails = ({ const history = useHistory(); - useEffect(() => { - const fetchQueryDetails = async () => { - const retrievedQuery = await retrieveQueryById(core, from, to, id); - setQuery(retrievedQuery); - }; + const fetchQueryDetails = async () => { + const retrievedQuery = await retrieveQueryById(core, getDataSourceFromUrl().id, from, to, id); + setQuery(retrievedQuery); + }; + useEffect(() => { if (id && from && to) { fetchQueryDetails(); } @@ -149,6 +159,17 @@ export const QueryGroupDetails = ({ } /> + {}} + onSelectedDataSource={fetchQueryDetails} + dataSourcePickerReadOnly={true} + /> diff --git a/public/pages/QueryInsights/QueryInsights.test.tsx b/public/pages/QueryInsights/QueryInsights.test.tsx index d6708e3..8552a77 100644 --- a/public/pages/QueryInsights/QueryInsights.test.tsx +++ b/public/pages/QueryInsights/QueryInsights.test.tsx @@ -9,6 +9,7 @@ import '@testing-library/jest-dom/extend-expect'; import QueryInsights from './QueryInsights'; import { MemoryRouter } from 'react-router-dom'; import { MockQueries } from '../../../test/testUtils'; +import { DataSourceContext } from '../TopNQueries/TopNQueries'; // Mock functions and data const mockOnTimeChange = jest.fn(); @@ -18,21 +19,38 @@ const mockCore = { }, }; +const dataSourceMenuMock = jest.fn(() =>
Mock DataSourceMenu
); + +const dataSourceManagementMock = { + ui: { + getDataSourceMenu: jest.fn().mockReturnValue(dataSourceMenuMock), + }, +}; +const mockDataSourceContext = { + dataSource: { id: 'test', label: 'Test' }, + setDataSource: jest.fn(), +}; + const sampleQueries = MockQueries(); const renderQueryInsights = () => render( - + + + ); diff --git a/public/pages/QueryInsights/QueryInsights.tsx b/public/pages/QueryInsights/QueryInsights.tsx index c4ec1ae..69a5591 100644 --- a/public/pages/QueryInsights/QueryInsights.tsx +++ b/public/pages/QueryInsights/QueryInsights.tsx @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { EuiBasicTableColumn, EuiInMemoryTable, EuiLink, EuiSuperDatePicker } from '@elastic/eui'; import { useHistory, useLocation } from 'react-router-dom'; -import { CoreStart } from 'opensearch-dashboards/public'; -import { QUERY_INSIGHTS } from '../TopNQueries/TopNQueries'; +import { AppMountParameters, CoreStart } from 'opensearch-dashboards/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; +import { DataSourceContext, QUERY_INSIGHTS } from '../TopNQueries/TopNQueries'; import { SearchQueryRecord } from '../../../types/types'; import { CPU_TIME, @@ -24,6 +25,8 @@ import { } from '../../../common/constants'; import { calculateMetric } from '../Utils/MetricUtils'; import { parseDateString } from '../Utils/DateUtils'; +import { QueryInsightsDataSourceMenu } from '../../components/DataSourcePicker'; +import { QueryInsightsDashboardsPluginStartDependencies } from '../../types'; const TIMESTAMP_FIELD = 'timestamp'; const MEASUREMENTS_FIELD = 'measurements'; @@ -42,6 +45,10 @@ const QueryInsights = ({ currStart, currEnd, core, + depsStart, + params, + retrieveQueries, + dataSourceManagement, }: { queries: SearchQueryRecord[]; loading: boolean; @@ -50,6 +57,10 @@ const QueryInsights = ({ currStart: string; currEnd: string; core: CoreStart; + params: AppMountParameters; + dataSourceManagement?: DataSourceManagementPluginSetup; + retrieveQueries?: any; + depsStart: QueryInsightsDashboardsPluginStartDependencies; }) => { const history = useHistory(); const location = useLocation(); @@ -57,6 +68,7 @@ const QueryInsights = ({ const from = parseDateString(currStart); const to = parseDateString(currEnd); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; useEffect(() => { core.chrome.setBreadcrumbs([ @@ -258,110 +270,125 @@ const QueryInsights = ({ ); return ( - setPagination({ pageIndex: index })} - pagination={pagination} - loading={loading} - search={{ - box: { - placeholder: 'Search queries', - schema: false, - }, - filters: [ - { - type: 'field_value_selection', - field: GROUP_BY_FIELD, - name: TYPE, - multiSelect: true, - options: [ - { - value: 'NONE', - name: 'query', - view: 'query', - }, - { - value: 'SIMILARITY', - name: 'group', - view: 'group', - }, - ], - noOptionsMessage: 'No data available for the selected type', // Fallback message when no queries match - }, - { - type: 'field_value_selection', - field: INDICES_FIELD, - name: INDICES, - multiSelect: true, - options: filterDuplicates( - queries.map((query) => { - const values = Array.from(new Set(query[INDICES_FIELD].flat())); - return { - value: values.join(','), - name: values.join(','), - view: values.join(', '), - }; - }) - ), - }, - { - type: 'field_value_selection', - field: SEARCH_TYPE_FIELD, - name: SEARCH_TYPE, - multiSelect: false, - options: filterDuplicates( - queries.map((query) => ({ - value: query[SEARCH_TYPE_FIELD], - name: query[SEARCH_TYPE_FIELD], - view: query[SEARCH_TYPE_FIELD], - })) - ), + <> + {}} + onSelectedDataSource={() => { + retrieveQueries(currStart, currEnd); + }} + dataSourcePickerReadOnly={false} + /> + ({ - value: query[NODE_ID_FIELD], - name: query[NODE_ID_FIELD], - view: query[NODE_ID_FIELD].replaceAll('_', ' '), - })) - ), + }} + onTableChange={({ page: { index } }) => setPagination({ pageIndex: index })} + pagination={pagination} + loading={loading} + search={{ + box: { + placeholder: 'Search queries', + schema: false, }, - ], - toolsRight: [ - , - ], - }} - executeQueryOptions={{ - defaultFields: [ - TIMESTAMP_FIELD, - MEASUREMENTS_FIELD, - INDICES_FIELD, - SEARCH_TYPE_FIELD, - NODE_ID_FIELD, - TOTAL_SHARDS_FIELD, - ], - }} - allowNeutralSort={false} - itemId={(query) => `${query.id}-${query.timestamp}`} - /> + filters: [ + { + type: 'field_value_selection', + field: GROUP_BY_FIELD, + name: TYPE, + multiSelect: true, + options: [ + { + value: 'NONE', + name: 'query', + view: 'query', + }, + { + value: 'SIMILARITY', + name: 'group', + view: 'group', + }, + ], + noOptionsMessage: 'No data available for the selected type', // Fallback message when no queries match + }, + { + type: 'field_value_selection', + field: INDICES_FIELD, + name: INDICES, + multiSelect: true, + options: filterDuplicates( + queries.map((query) => { + const values = Array.from(new Set(query[INDICES_FIELD].flat())); + return { + value: values.join(','), + name: values.join(','), + view: values.join(', '), + }; + }) + ), + }, + { + type: 'field_value_selection', + field: SEARCH_TYPE_FIELD, + name: SEARCH_TYPE, + multiSelect: false, + options: filterDuplicates( + queries.map((query) => ({ + value: query[SEARCH_TYPE_FIELD], + name: query[SEARCH_TYPE_FIELD], + view: query[SEARCH_TYPE_FIELD], + })) + ), + }, + { + type: 'field_value_selection', + field: NODE_ID_FIELD, + name: NODE_ID, + multiSelect: true, + options: filterDuplicates( + queries.map((query) => ({ + value: query[NODE_ID_FIELD], + name: query[NODE_ID_FIELD], + view: query[NODE_ID_FIELD].replaceAll('_', ' '), + })) + ), + }, + ], + toolsRight: [ + , + ], + }} + executeQueryOptions={{ + defaultFields: [ + TIMESTAMP_FIELD, + MEASUREMENTS_FIELD, + INDICES_FIELD, + SEARCH_TYPE_FIELD, + NODE_ID_FIELD, + TOTAL_SHARDS_FIELD, + ], + }} + allowNeutralSort={false} + itemId={(query) => `${query.id}-${query.timestamp}`} + /> + ); }; diff --git a/public/pages/TopNQueries/TopNQueries.test.tsx b/public/pages/TopNQueries/TopNQueries.test.tsx index cbbb238..66937fc 100644 --- a/public/pages/TopNQueries/TopNQueries.test.tsx +++ b/public/pages/TopNQueries/TopNQueries.test.tsx @@ -57,7 +57,7 @@ const setUpDefaultEnabledSettings = () => { const renderTopNQueries = (type: string) => render( - + ); @@ -99,7 +99,9 @@ describe('TopNQueries Component', () => { (mockCore.http.get as jest.Mock).mockResolvedValueOnce(mockSettingsResponse); const container = renderTopNQueries(CONFIGURATION); await waitFor(() => { - expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings'); + expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings', { + query: { dataSourceId: undefined }, + }); expect(screen.getByText('Mocked Configuration')).toBeInTheDocument(); expect(container).toMatchSnapshot(); }); @@ -110,7 +112,7 @@ describe('TopNQueries Component', () => { const container = renderTopNQueries(QUERY_INSIGHTS); await waitFor(() => { // Verify each endpoint is called - expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings'); + expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings', expect.any(Object)); expect(mockCore.http.get).toHaveBeenCalledWith( '/api/top_queries/latency', expect.any(Object) @@ -153,7 +155,7 @@ describe('TopNQueries Component', () => { const container = renderTopNQueries(QUERY_INSIGHTS); await waitFor(() => { // Verify each endpoint is called - expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings'); + expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings', expect.any(Object)); expect(mockCore.http.get).toHaveBeenCalledWith( '/api/top_queries/latency', expect.any(Object) @@ -179,7 +181,13 @@ describe('TopNQueries Component', () => { // Render with initial time range const { rerender } = render( - + ); // Mock a new response for the time range update @@ -190,14 +198,20 @@ describe('TopNQueries Component', () => { // Re-render with updated time range to simulate a change rerender( - + ); // Verify that the component re-fetches data for the new time range await waitFor(() => { // 1 initial call for settings, 3 each for the initial rendering and re-rendering expect(mockCore.http.get).toHaveBeenCalledTimes(7); - expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings'); + expect(mockCore.http.get).toHaveBeenCalledWith('/api/settings', expect.any(Object)); expect(mockCore.http.get).toHaveBeenCalledWith( '/api/top_queries/latency', expect.any(Object) diff --git a/public/pages/TopNQueries/TopNQueries.tsx b/public/pages/TopNQueries/TopNQueries.tsx index e112eb8..9690e0a 100644 --- a/public/pages/TopNQueries/TopNQueries.tsx +++ b/public/pages/TopNQueries/TopNQueries.tsx @@ -3,10 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { createContext, useCallback, useEffect, useState } from 'react'; import { Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom'; import { EuiTab, EuiTabs, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { CoreStart } from 'opensearch-dashboards/public'; +import { AppMountParameters, CoreStart } from 'opensearch-dashboards/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; +import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; import QueryInsights from '../QueryInsights/QueryInsights'; import Configuration from '../Configuration/Configuration'; import QueryDetails from '../QueryDetails/QueryDetails'; @@ -24,6 +26,7 @@ import { import { MetricSettingsResponse } from '../../types'; import { getTimeAndUnitFromString } from '../Utils/MetricUtils'; import { parseDateString } from '../Utils/DateUtils'; +import { getDataSourceFromUrl } from '../../components/DataSourcePicker'; export const QUERY_INSIGHTS = '/queryInsights'; export const CONFIGURATION = '/configuration'; @@ -39,14 +42,27 @@ export interface GroupBySettings { groupBy: string; } +export interface DataSourceContextType { + dataSource: DataSourceOption; + setDataSource: React.Dispatch>; +} + +// export const LocalCluster = { label: 'Local cluster', id: '' }; + +export const DataSourceContext = createContext(null); + const TopNQueries = ({ core, depsStart, + params, + dataSourceManagement, initialStart = 'now-1d', initialEnd = 'now', }: { core: CoreStart; depsStart: QueryInsightsDashboardsPluginStartDependencies; + params: AppMountParameters; + dataSourceManagement?: DataSourceManagementPluginSetup; initialStart?: string; initialEnd?: string; }) => { @@ -127,13 +143,15 @@ const TopNQueries = ({ ); + // TODO: refactor retrieveQueries and retrieveConfigInfo into a Util function const retrieveQueries = useCallback( async (start: string, end: string) => { const nullResponse = { response: { top_queries: [] } }; - const params = { + const apiParams = { query: { from: parseDateString(start), to: parseDateString(end), + dataSourceId: getDataSourceFromUrl().id, // TODO: get this dynamically from the URL }, }; const fetchMetric = async (endpoint: string) => { @@ -141,7 +159,7 @@ const TopNQueries = ({ // TODO: #13 refactor the interface definitions for requests and responses const response: { response: { top_queries: SearchQueryRecord[] } } = await core.http.get( endpoint, - params + apiParams ); return { response: { @@ -215,7 +233,10 @@ const TopNQueries = ({ return transient ?? persistent; }; - const resp = await core.http.get('/api/settings'); + // const resp = await core.http.get('/api/settings', {query: {dataSourceId: '738ffbd0-d8de-11ef-9d96-eff1abd421b8'}}); + const resp = await core.http.get('/api/settings', { + query: { dataSourceId: getDataSourceFromUrl().id }, + }); const persistentSettings = resp?.response?.persistent?.search?.insights?.top_queries; const transientSettings = resp?.response?.transient?.search?.insights?.top_queries; const metrics = [ @@ -252,6 +273,10 @@ const TopNQueries = ({ currWindowSize: time, currTimeUnit: timeUnits, }); + } else { + setMetricSettings(metricType, { + isEnabled: false, + }); } }); const groupBy = getMergedGroupBySettings( @@ -280,6 +305,7 @@ const TopNQueries = ({ top_n_size: newTopN, window_size: `${newWindowSize}${newTimeUnit === 'MINUTES' ? 'm' : 'h'}`, group_by: newGroupBy, + dataSourceId: getDataSourceFromUrl().id, // TODO: get this dynamically from the URL }, }); } catch (error) { @@ -311,72 +337,99 @@ const TopNQueries = ({ retrieveQueries(currStart, currEnd); }, [latencySettings, cpuSettings, memorySettings, currStart, currEnd, retrieveQueries]); + const dataSourceFromUrl = getDataSourceFromUrl(); + + const [dataSource, setDataSource] = useState(dataSourceFromUrl); + return ( -
- - - {() => { - return ; - }} - - - {() => { - return ; - }} - - - - -

Query insights - Top N queries

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

Query insights - Configuration

-
- - - } - /> + +
+ + + {() => { + return ( + + ); + }} + + + {() => { + return ( + + ); + }} + + + + +

Query insights - Top N queries

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

Query insights - Configuration

+
+ + + } + /> - {tabs.map(renderTab)} - - -
- -
-
+ {tabs.map(renderTab)} + + +
+ +
+
+ ); }; diff --git a/public/pages/Utils/QueryUtils.ts b/public/pages/Utils/QueryUtils.ts index 227a012..c615920 100644 --- a/public/pages/Utils/QueryUtils.ts +++ b/public/pages/Utils/QueryUtils.ts @@ -8,6 +8,7 @@ import { SearchQueryRecord } from '../../../types/types'; // Utility function to fetch query by id and time range export const retrieveQueryById = async ( core: { http: { get: (endpoint: string, params: any) => Promise } }, + dataSourceId: string, start: string | null, end: string | null, id: string | null @@ -15,6 +16,7 @@ export const retrieveQueryById = async ( const nullResponse = { response: { top_queries: [] } }; const params = { query: { + dataSourceId, from: start, to: end, id, diff --git a/public/plugin.ts b/public/plugin.ts index f1f42e1..3ddf161 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -12,6 +12,7 @@ import { } from '../../../src/core/public'; import { QueryInsightsDashboardsPluginSetup, + QueryInsightsDashboardsPluginSetupDependencies, QueryInsightsDashboardsPluginStart, QueryInsightsDashboardsPluginStartDependencies, } from './types'; @@ -25,7 +26,10 @@ export class QueryInsightsDashboardsPlugin {}, QueryInsightsDashboardsPluginStartDependencies > { - public setup(core: CoreSetup): QueryInsightsDashboardsPluginSetup { + public setup( + core: CoreSetup, + deps: QueryInsightsDashboardsPluginSetupDependencies + ): QueryInsightsDashboardsPluginSetup { // Register an application into the side navigation menu core.application.register({ id: PLUGIN_NAME, @@ -48,7 +52,8 @@ export class QueryInsightsDashboardsPlugin return renderApp( coreStart, depsStart as QueryInsightsDashboardsPluginStartDependencies, - params + params, + deps.dataSourceManagement ); }, }); diff --git a/public/types.ts b/public/types.ts index 81ef52d..d8374ae 100644 --- a/public/types.ts +++ b/public/types.ts @@ -3,12 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { DataSourcePluginStart } from '../../../src/plugins/data_source/public'; +import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; /* eslint-disable @typescript-eslint/no-empty-interface */ export interface QueryInsightsDashboardsPluginSetup {} export interface QueryInsightsDashboardsPluginStart {} -export interface QueryInsightsDashboardsPluginStartDependencies {} +export interface QueryInsightsDashboardsPluginStartDependencies { + dataSource?: DataSourcePluginStart; +} +export interface QueryInsightsDashboardsPluginSetupDependencies { + dataSourceManagement?: DataSourceManagementPluginSetup; +} /* eslint-enable @typescript-eslint/no-empty-interface */ export interface MetricSettingsResponse { @@ -19,4 +26,5 @@ export interface MetricSettingsResponse { export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; + dataSource?: DataSourcePluginStart; } diff --git a/server/plugin.ts b/server/plugin.ts index fa7755f..0ed54d2 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -15,6 +15,11 @@ import { QueryInsightsPlugin } from './clusters/queryInsightsPlugin'; import { QueryInsightsDashboardsPluginSetup, QueryInsightsDashboardsPluginStart } from './types'; import { defineRoutes } from './routes'; +import { DataSourcePluginSetup } from '../../../src/plugins/data_source/server/types'; + +export interface QueryInsightsDashboardsPluginSetupDependencies { + dataSource: DataSourcePluginSetup; +} export class QueryInsightsDashboardsPlugin implements Plugin { @@ -24,8 +29,8 @@ export class QueryInsightsDashboardsPlugin this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { - this.logger.debug('query-insights-dashboards: Setup'); + public setup(core: CoreSetup, { dataSource }: QueryInsightsDashboardsPluginSetupDependencies) { + const dataSourceEnabled = !!dataSource; const router = core.http.createRouter(); const queryInsightsClient: ILegacyCustomClusterClient = core.opensearch.legacy.createClient( 'opensearch_queryInsights', @@ -33,6 +38,10 @@ export class QueryInsightsDashboardsPlugin plugins: [QueryInsightsPlugin], } ); + if (dataSourceEnabled) { + dataSource.registerCustomApiSchema(QueryInsightsPlugin); + } + // @ts-ignore core.http.registerRouteHandlerContext('queryInsights_plugin', (_context, _request) => { return { @@ -42,7 +51,7 @@ export class QueryInsightsDashboardsPlugin }); // Register server side APIs - defineRoutes(router); + defineRoutes(router, dataSourceEnabled); return {}; } diff --git a/server/routes/index.ts b/server/routes/index.ts index 5dcf314..3c2455d 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -5,24 +5,43 @@ import { schema } from '@osd/config-schema'; import { IRouter } from '../../../../src/core/server'; -export function defineRoutes(router: IRouter) { +export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { router.get( { path: '/api/top_queries', - validate: false, + validate: { + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), + }, }, async (context, request, response) => { try { - const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) - .callAsCurrentUser; - const res = await client('queryInsights.getTopNQueries'); - return response.custom({ - statusCode: 200, - body: { - ok: true, - response: res, - }, - }); + // data source disabled + if (!dataSourceEnabled || !request.query?.dataSourceId) { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.getTopNQueries'); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } else { + const client = context.dataSource.opensearch.legacy.getClient( + request.query?.dataSourceId + ); + const res = await client.callAPI('queryInsights.getTopNQueries', {}); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } } catch (error) { console.error('Unable to get top queries: ', error); return response.ok({ @@ -43,6 +62,7 @@ export function defineRoutes(router: IRouter) { from: schema.maybe(schema.string({ defaultValue: '' })), to: schema.maybe(schema.string({ defaultValue: '' })), id: schema.maybe(schema.string({ defaultValue: '' })), + dataSourceId: schema.maybe(schema.string()), }), }, }, @@ -50,20 +70,36 @@ export function defineRoutes(router: IRouter) { try { const { from, to, id } = request.query; const params = { from, to, id }; - const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) - .callAsCurrentUser; - - const res = - id != null - ? await client('queryInsights.getTopNQueriesLatencyForId', params) - : await client('queryInsights.getTopNQueriesLatency', params); - return response.custom({ - statusCode: 200, - body: { - ok: true, - response: res, - }, - }); + if (!dataSourceEnabled || !request.query?.dataSourceId) { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = + id != null + ? await client('queryInsights.getTopNQueriesLatencyForId', params) + : await client('queryInsights.getTopNQueriesLatency', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } else { + const client = context.dataSource.opensearch.legacy.getClient( + request.query?.dataSourceId + ); + const res = + id != null + ? await client.callAPI('queryInsights.getTopNQueriesLatencyForId', params) + : await client.callAPI('queryInsights.getTopNQueriesLatency', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } } catch (error) { console.error('Unable to get top queries (latency): ', error); return response.ok({ @@ -84,6 +120,7 @@ export function defineRoutes(router: IRouter) { from: schema.maybe(schema.string({ defaultValue: '' })), to: schema.maybe(schema.string({ defaultValue: '' })), id: schema.maybe(schema.string({ defaultValue: '' })), + dataSourceId: schema.maybe(schema.string()), }), }, }, @@ -91,20 +128,36 @@ export function defineRoutes(router: IRouter) { try { const { from, to, id } = request.query; const params = { from, to, id }; - const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) - .callAsCurrentUser; - - const res = - id != null - ? await client('queryInsights.getTopNQueriesCpuForId', params) - : await client('queryInsights.getTopNQueriesCpu', params); - return response.custom({ - statusCode: 200, - body: { - ok: true, - response: res, - }, - }); + if (!dataSourceEnabled || !request.query?.dataSourceId) { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = + id != null + ? await client('queryInsights.getTopNQueriesCpuForId', params) + : await client('queryInsights.getTopNQueriesCpu', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } else { + const client = context.dataSource.opensearch.legacy.getClient( + request.query?.dataSourceId + ); + const res = + id != null + ? await client.callAPI('queryInsights.getTopNQueriesCpuForId', params) + : await client.callAPI('queryInsights.getTopNQueriesCpu', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } } catch (error) { console.error('Unable to get top queries (cpu): ', error); return response.ok({ @@ -125,6 +178,7 @@ export function defineRoutes(router: IRouter) { from: schema.maybe(schema.string({ defaultValue: '' })), to: schema.maybe(schema.string({ defaultValue: '' })), id: schema.maybe(schema.string({ defaultValue: '' })), + dataSourceId: schema.maybe(schema.string()), }), }, }, @@ -132,20 +186,36 @@ export function defineRoutes(router: IRouter) { try { const { from, to, id } = request.query; const params = { from, to, id }; - const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) - .callAsCurrentUser; - - const res = - id != null - ? await client('queryInsights.getTopNQueriesMemoryForId', params) - : await client('queryInsights.getTopNQueriesMemory', params); - return response.custom({ - statusCode: 200, - body: { - ok: true, - response: res, - }, - }); + if (!dataSourceEnabled || !request.query?.dataSourceId) { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = + id != null + ? await client('queryInsights.getTopNQueriesMemoryForId', params) + : await client('queryInsights.getTopNQueriesMemory', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } else { + const client = context.dataSource.opensearch.legacy.getClient( + request.query?.dataSourceId + ); + const res = + id != null + ? await client.callAPI('queryInsights.getTopNQueriesMemoryForId', params) + : await client.callAPI('queryInsights.getTopNQueriesMemory', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } } catch (error) { console.error('Unable to get top queries (memory): ', error); return response.ok({ @@ -161,20 +231,38 @@ export function defineRoutes(router: IRouter) { router.get( { path: '/api/settings', - validate: false, + validate: { + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), + }, }, async (context, request, response) => { try { - const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) - .callAsCurrentUser; - const res = await client('queryInsights.getSettings'); - return response.custom({ - statusCode: 200, - body: { - ok: true, - response: res, - }, - }); + if (!dataSourceEnabled || !request.query?.dataSourceId) { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.getSettings'); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } else { + const client = context.dataSource.opensearch.legacy.getClient( + request.query?.dataSourceId + ); + const res = await client.callAPI('queryInsights.getSettings', {}); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } } catch (error) { console.error('Unable to get top queries: ', error); return response.ok({ @@ -197,14 +285,13 @@ export function defineRoutes(router: IRouter) { top_n_size: schema.maybe(schema.string({ defaultValue: '' })), window_size: schema.maybe(schema.string({ defaultValue: '' })), group_by: schema.maybe(schema.string({ defaultValue: '' })), + dataSourceId: schema.maybe(schema.string()), }), }, }, async (context, request, response) => { try { const query = request.query; - const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) - .callAsCurrentUser; const params = { body: { persistent: { @@ -215,14 +302,30 @@ export function defineRoutes(router: IRouter) { }, }, }; - const res = await client('queryInsights.setSettings', params); - return response.custom({ - statusCode: 200, - body: { - ok: true, - response: res, - }, - }); + if (!dataSourceEnabled || !request.query?.dataSourceId) { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.setSettings', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } else { + const client = context.dataSource.opensearch.legacy.getClient( + request.query?.dataSourceId + ); + const res = await client.callAPI('queryInsights.setSettings', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } } catch (error) { console.error('Unable to set settings: ', error); return response.ok({