Skip to content

Commit e4ccdb8

Browse files
ansjcyderek-hodzane17
authored
MDS support for query insights dashboards (#71)
* Add support for MDS Signed-off-by: Derek Ho <[email protected]> * MDS for all paths, pages with dummy dataSourceId Signed-off-by: David Zane <[email protected]> * Fix all pages and apis Signed-off-by: Derek Ho <[email protected]> * Push data source into the url Signed-off-by: Derek Ho <[email protected]> * Persist data source selection across pages Signed-off-by: David Zane <[email protected]> * fix when local cluster id is empty Signed-off-by: Chenyang Ji <[email protected]> * fix manage link based on OpenSearch-Dashboards/pull/9059 Signed-off-by: Chenyang Ji <[email protected]> * refresh page data when data source is changed Signed-off-by: Chenyang Ji <[email protected]> * fix lint Signed-off-by: Chenyang Ji <[email protected]> * fix unit tests and cypress tests Signed-off-by: Chenyang Ji <[email protected]> * make data source picker read only on detail pages Signed-off-by: Chenyang Ji <[email protected]> --------- Signed-off-by: Derek Ho <[email protected]> Signed-off-by: David Zane <[email protected]> Signed-off-by: Chenyang Ji <[email protected]> Co-authored-by: Derek Ho <[email protected]> Co-authored-by: David Zane <[email protected]>
1 parent 3577141 commit e4ccdb8

22 files changed

+852
-338
lines changed

Diff for: cypress/e2e/3_configurations.cy.js

+26-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ const clearAll = () => {
1111
cy.disableTopQueries(METRICS.MEMORY);
1212
};
1313

14+
const toggleMetricEnabled = async () => {
15+
cy.get('button[data-test-subj="top-n-metric-toggle"]').trigger('mouseover');
16+
cy.wait(1000);
17+
cy.get('button[data-test-subj="top-n-metric-toggle"]').click({ force: true });
18+
cy.wait(1000);
19+
};
20+
1421
describe('Query Insights Configurations Page', () => {
1522
beforeEach(() => {
1623
clearAll();
@@ -66,22 +73,30 @@ describe('Query Insights Configurations Page', () => {
6673
*/
6774
it('should allow enabling and disabling metrics', () => {
6875
// Validate the switch for enabling/disabling metrics
69-
cy.get('button[role="switch"]').should('exist');
70-
// Toggle the switch
71-
cy.get('button[role="switch"]')
72-
.first()
73-
.should('have.attr', 'aria-checked', 'false') // Initially disabled
74-
.click()
75-
.should('have.attr', 'aria-checked', 'true'); // After toggling, it should be enabled
76+
cy.get('button[data-test-subj="top-n-metric-toggle"]')
77+
.should('exist')
78+
.and('have.attr', 'aria-checked', 'false') // Initially disabled)
79+
.trigger('mouseover')
80+
.click();
81+
cy.wait(1000);
82+
83+
cy.get('button[data-test-subj="top-n-metric-toggle"]').should(
84+
'have.attr',
85+
'aria-checked',
86+
'true'
87+
); // After toggling, it should be enabled
7688
// Re-enable the switch
77-
cy.get('button[role="switch"]').first().click().should('have.attr', 'aria-checked', 'false');
89+
cy.get('button[data-test-subj="top-n-metric-toggle"]')
90+
.trigger('mouseover')
91+
.click()
92+
.should('have.attr', 'aria-checked', 'false');
7893
});
7994

8095
/**
8196
* Validate the value of N (count) input
8297
*/
8398
it('should allow updating the value of N (count)', () => {
84-
cy.get('button[role="switch"]').first().click();
99+
toggleMetricEnabled();
85100
// Locate the input for N
86101
cy.get('input[type="number"]').should('have.attr', 'value', '3'); // Default 3
87102
// Change the value to 50
@@ -95,7 +110,7 @@ describe('Query Insights Configurations Page', () => {
95110
* Validate the window size dropdowns
96111
*/
97112
it('should allow selecting a window size and unit', () => {
98-
cy.get('button[role="switch"]').first().click();
113+
toggleMetricEnabled();
99114
// Validate default values
100115
cy.get('select#timeUnit').should('have.value', 'MINUTES'); // Default unit is "Minute(s)"
101116
// Test valid time unit selection
@@ -192,7 +207,7 @@ describe('Query Insights Configurations Page', () => {
192207
* After saving the status panel should show the correct status
193208
*/
194209
it('should allow saving the configuration', () => {
195-
cy.get('button[role="switch"]').first().click();
210+
toggleMetricEnabled();
196211
cy.get('select#timeUnit').select('MINUTES');
197212
cy.get('select#minutes').select('5');
198213
cy.get('button[data-test-subj="save-config-button"]').click();

Diff for: opensearch_dashboards.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"server": true,
66
"ui": true,
77
"requiredPlugins": ["navigation"],
8-
"optionalPlugins": []
8+
"optionalPlugins": ["dataSource",
9+
"dataSourceManagement"]
910
}

Diff for: public/application.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,25 @@ import { HashRouter as Router } from 'react-router-dom';
99
import { AppMountParameters, CoreStart } from '../../../src/core/public';
1010
import { QueryInsightsDashboardsApp } from './components/app';
1111
import { QueryInsightsDashboardsPluginStartDependencies } from './types';
12+
import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public';
1213

1314
export const renderApp = (
1415
core: CoreStart,
1516
depsStart: QueryInsightsDashboardsPluginStartDependencies,
16-
{ element }: AppMountParameters
17+
params: AppMountParameters,
18+
dataSourceManagement?: DataSourceManagementPluginSetup
1719
) => {
1820
ReactDOM.render(
1921
<Router>
20-
<QueryInsightsDashboardsApp core={core} depsStart={depsStart} />
22+
<QueryInsightsDashboardsApp
23+
core={core}
24+
depsStart={depsStart}
25+
params={params}
26+
dataSourceManagement={dataSourceManagement}
27+
/>
2128
</Router>,
22-
element
29+
params.element
2330
);
2431

25-
return () => ReactDOM.unmountComponentAtNode(element);
32+
return () => ReactDOM.unmountComponentAtNode(params.element);
2633
};

Diff for: public/components/DataSourcePicker.tsx

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import {
8+
DataSourceManagementPluginSetup,
9+
DataSourceOption,
10+
DataSourceSelectableConfig,
11+
} from 'src/plugins/data_source_management/public';
12+
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
13+
import { QueryInsightsDashboardsPluginStartDependencies } from '../types';
14+
15+
export interface DataSourceMenuProps {
16+
dataSourceManagement: DataSourceManagementPluginSetup;
17+
depsStart: QueryInsightsDashboardsPluginStartDependencies;
18+
coreStart: CoreStart;
19+
params: AppMountParameters;
20+
setDataSource: React.Dispatch<React.SetStateAction<DataSourceOption>>;
21+
selectedDataSource: DataSourceOption;
22+
onManageDataSource: () => void;
23+
onSelectedDataSource: () => void;
24+
dataSourcePickerReadOnly: boolean;
25+
}
26+
27+
export function getDataSourceEnabledUrl(dataSource: DataSourceOption) {
28+
const url = new URL(window.location.href);
29+
url.searchParams.set('dataSource', JSON.stringify(dataSource));
30+
return url;
31+
}
32+
33+
export function getDataSourceFromUrl(): DataSourceOption {
34+
const urlParams = new URLSearchParams(window.location.search);
35+
const dataSourceParam = (urlParams && urlParams.get('dataSource')) || '{}';
36+
// following block is needed if the dataSource param is set to non-JSON value, say 'undefined'
37+
try {
38+
return JSON.parse(dataSourceParam);
39+
} catch (e) {
40+
return JSON.parse('{}'); // Return an empty object or some default value if parsing fails
41+
}
42+
}
43+
44+
export const QueryInsightsDataSourceMenu = React.memo(
45+
(props: DataSourceMenuProps) => {
46+
const {
47+
coreStart,
48+
depsStart,
49+
dataSourceManagement,
50+
params,
51+
setDataSource,
52+
selectedDataSource,
53+
onManageDataSource,
54+
onSelectedDataSource,
55+
dataSourcePickerReadOnly,
56+
} = props;
57+
const { setHeaderActionMenu } = params;
58+
const DataSourceMenu = dataSourceManagement?.ui.getDataSourceMenu<DataSourceSelectableConfig>();
59+
60+
const dataSourceEnabled = !!depsStart.dataSource?.dataSourceEnabled;
61+
62+
const wrapSetDataSourceWithUpdateUrl = (dataSources: DataSourceOption[]) => {
63+
window.history.replaceState({}, '', getDataSourceEnabledUrl(dataSources[0]).toString());
64+
setDataSource(dataSources[0]);
65+
onSelectedDataSource();
66+
};
67+
68+
return dataSourceEnabled ? (
69+
<DataSourceMenu
70+
onManageDataSource={onManageDataSource}
71+
setMenuMountPoint={setHeaderActionMenu}
72+
componentType={dataSourcePickerReadOnly ? 'DataSourceView' : 'DataSourceSelectable'}
73+
componentConfig={{
74+
onManageDataSource,
75+
savedObjects: coreStart.savedObjects.client,
76+
notifications: coreStart.notifications,
77+
activeOption:
78+
selectedDataSource.id || selectedDataSource.label ? [selectedDataSource] : undefined,
79+
onSelectedDataSources: wrapSetDataSourceWithUpdateUrl,
80+
fullWidth: true,
81+
}}
82+
/>
83+
) : null;
84+
},
85+
(prevProps, newProps) =>
86+
prevProps.selectedDataSource.id === newProps.selectedDataSource.id &&
87+
prevProps.dataSourcePickerReadOnly === newProps.dataSourcePickerReadOnly
88+
);

Diff for: public/components/app.test.tsx

+26-13
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,41 @@ import { render } from '@testing-library/react';
88
import { coreMock } from '../../../../src/core/public/mocks';
99
import { MemoryRouter as Router } from 'react-router-dom';
1010
import { QueryInsightsDashboardsApp } from './app';
11+
import { createMemoryHistory } from 'history';
1112

1213
describe('<QueryInsightsDashboardsApp /> spec', () => {
1314
it('renders the component', () => {
14-
const mockHttpStart = {
15-
basePath: {
16-
serverBasePath: '/app/opensearch-dashboards',
15+
const coreStart = coreMock.createStart();
16+
// Mock AppMountParameters
17+
const params = {
18+
appBasePath: '/',
19+
history: createMemoryHistory(),
20+
setHeaderActionMenu: jest.fn(),
21+
element: document.createElement('div'),
22+
};
23+
// Mock plugin dependencies
24+
const depsStart = {
25+
navigation: {
26+
ui: { TopNavMenu: () => null },
27+
},
28+
data: {
29+
dataSources: {
30+
dataSourceService: jest.fn(),
31+
},
1732
},
1833
};
19-
const coreStart = coreMock.createStart();
20-
2134
const { container } = render(
2235
<Router>
2336
<QueryInsightsDashboardsApp
24-
basename="/"
2537
core={coreStart}
26-
http={mockHttpStart as any}
27-
navigation={
28-
{
29-
ui: { TopNavMenu: () => null },
30-
} as any
31-
}
32-
notifications={{} as any}
38+
depsStart={depsStart}
39+
params={params}
40+
dataSourceManagement={{
41+
ui: {
42+
getDataSourceMenu: jest.fn(),
43+
getDataSourceSelector: jest.fn(),
44+
},
45+
}}
3346
/>
3447
</Router>
3548
);

Diff for: public/components/app.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,32 @@
55

66
import React from 'react';
77
import { Route } from 'react-router-dom';
8+
import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public';
89
import TopNQueries from '../pages/TopNQueries/TopNQueries';
9-
import { CoreStart } from '../../../../src/core/public';
10+
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
1011
import { QueryInsightsDashboardsPluginStartDependencies } from '../types';
1112

1213
export const QueryInsightsDashboardsApp = ({
1314
core,
1415
depsStart,
16+
params,
17+
dataSourceManagement,
1518
}: {
1619
core: CoreStart;
1720
depsStart: QueryInsightsDashboardsPluginStartDependencies;
21+
params: AppMountParameters;
22+
dataSourceManagement?: DataSourceManagementPluginSetup;
1823
}) => {
19-
return <Route render={() => <TopNQueries core={core} depsStart={depsStart} />} />;
24+
return (
25+
<Route
26+
render={() => (
27+
<TopNQueries
28+
core={core}
29+
depsStart={depsStart}
30+
params={params}
31+
dataSourceManagement={dataSourceManagement}
32+
/>
33+
)}
34+
/>
35+
);
2036
};

Diff for: public/pages/Configuration/Configuration.test.tsx

+26-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
88
import '@testing-library/jest-dom/extend-expect';
99
import { MemoryRouter } from 'react-router-dom';
1010
import Configuration from './Configuration';
11+
import { DataSourceContext } from '../TopNQueries/TopNQueries';
1112

1213
const mockConfigInfo = jest.fn();
1314
const mockCoreStart = {
@@ -39,17 +40,34 @@ const groupBySettings = {
3940
groupBy: 'SIMILARITY',
4041
};
4142

43+
const dataSourceMenuMock = jest.fn(() => <div>Mock DataSourceMenu</div>);
44+
45+
const dataSourceManagementMock = {
46+
ui: {
47+
getDataSourceMenu: jest.fn().mockReturnValue(dataSourceMenuMock),
48+
},
49+
};
50+
const mockDataSourceContext = {
51+
dataSource: { id: 'test', label: 'Test' },
52+
setDataSource: jest.fn(),
53+
};
54+
4255
const renderConfiguration = (overrides = {}) =>
4356
render(
4457
<MemoryRouter>
45-
<Configuration
46-
latencySettings={{ ...defaultLatencySettings, ...overrides }}
47-
cpuSettings={defaultCpuSettings}
48-
memorySettings={defaultMemorySettings}
49-
groupBySettings={groupBySettings}
50-
configInfo={mockConfigInfo}
51-
core={mockCoreStart}
52-
/>
58+
<DataSourceContext.Provider value={mockDataSourceContext}>
59+
<Configuration
60+
latencySettings={{ ...defaultLatencySettings, ...overrides }}
61+
cpuSettings={defaultCpuSettings}
62+
memorySettings={defaultMemorySettings}
63+
groupBySettings={groupBySettings}
64+
configInfo={mockConfigInfo}
65+
core={mockCoreStart}
66+
depsStart={{ navigation: {} }}
67+
params={{} as any}
68+
dataSourceManagement={dataSourceManagementMock}
69+
/>
70+
</DataSourceContext.Provider>
5371
</MemoryRouter>
5472
);
5573

0 commit comments

Comments
 (0)