From adcaba76cdd911c29e9b6a290cdad8962bb65e99 Mon Sep 17 00:00:00 2001 From: Wei Wang <93847013+weiwang118@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:44:59 -0700 Subject: [PATCH] [New]feat: add related connections and nested view to datasource table. (#7969) * feat: add related connections and nested view to datasource table. Signed-off-by: Wei Wang * update snapshot Signed-off-by: Wei Wang --------- Signed-off-by: Wei Wang Signed-off-by: Wei Wang Co-authored-by: Wei Wang --- .../data_source_home_panel.test.tsx | 18 +- .../data_source_home_panel.tsx | 13 +- .../data_source_table.test.tsx.snap | 1201 ++++-- .../data_source_table.test.tsx | 14 +- .../data_source_table/data_source_table.tsx | 104 +- ...query_data_connections_table.test.tsx.snap | 3335 ++++++++++++++--- .../direct_query_table.scss | 15 + ...rect_query_data_connections_table.test.tsx | 284 +- ...ge_direct_query_data_connections_table.tsx | 551 +-- .../public/components/utils.ts | 83 +- .../data_source_management/public/mocks.ts | 35 +- .../data_source_management/public/types.ts | 25 +- 12 files changed, 4440 insertions(+), 1238 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/direct_query_table.scss diff --git a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx index aab2971ae7eb..04704ffa913e 100644 --- a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx @@ -9,7 +9,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiTab } from '@elastic/eui'; import { DataSourceHomePanel } from './data_source_home_panel'; import { DataSourceTableWithRouter } from '../data_source_table/data_source_table'; -import { ManageDirectQueryDataConnectionsTable } from '../direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table'; +import { ManageDirectQueryDataConnectionsTableWithRouter } from '../direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { getListBreadcrumbs } from '../breadcrumbs'; import { navigationPluginMock } from 'src/plugins/navigation/public/mocks'; @@ -22,7 +22,9 @@ jest.mock('../data_source_table/data_source_table', () => ({ jest.mock( '../direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table', () => ({ - ManageDirectQueryDataConnectionsTable: () =>
ManageDirectQueryDataConnectionsTable
, + ManageDirectQueryDataConnectionsTableWithRouter: () => ( +
ManageDirectQueryDataConnectionsTableWithRouter
+ ), }) ); jest.mock('../create_button', () => ({ @@ -67,8 +69,10 @@ describe('DataSourceHomePanel', () => { match: {} as any, }; - const shallowComponent = (props = defaultProps) => shallow(); - const mountComponent = (props = defaultProps) => mount(); + const shallowComponent = (props = defaultProps) => + shallow(); + const mountComponent = (props = defaultProps) => + mount(); test('renders correctly', () => { const wrapper = shallowComponent(); @@ -79,13 +83,13 @@ describe('DataSourceHomePanel', () => { const wrapper = mountComponent(); wrapper.find(EuiTab).at(0).simulate('click'); expect(wrapper.find(DataSourceTableWithRouter)).toHaveLength(1); - expect(wrapper.find(ManageDirectQueryDataConnectionsTable)).toHaveLength(0); + expect(wrapper.find(ManageDirectQueryDataConnectionsTableWithRouter)).toHaveLength(0); }); test('renders ManageDirectQueryDataConnectionsTable when manageDirectQueryDataSources tab is selected', () => { const wrapper = mountComponent(); wrapper.find(EuiTab).at(1).simulate('click'); - expect(wrapper.find(ManageDirectQueryDataConnectionsTable)).toHaveLength(1); + expect(wrapper.find(ManageDirectQueryDataConnectionsTableWithRouter)).toHaveLength(1); expect(wrapper.find(DataSourceTableWithRouter)).toHaveLength(0); }); @@ -93,7 +97,7 @@ describe('DataSourceHomePanel', () => { const wrapper = mountComponent(); expect(wrapper.find(DataSourceTableWithRouter)).toHaveLength(1); wrapper.find(EuiTab).at(1).simulate('click'); - expect(wrapper.find(ManageDirectQueryDataConnectionsTable)).toHaveLength(1); + expect(wrapper.find(ManageDirectQueryDataConnectionsTableWithRouter)).toHaveLength(1); }); test('does not render OpenSearch connections tab when featureFlagStatus is false', () => { diff --git a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx index 918d85d8b2cc..85e83f0a35b4 100644 --- a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx +++ b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx @@ -21,7 +21,7 @@ import { useObservable } from 'react-use'; import { of } from 'rxjs'; import { DataSourceHeader } from './data_source_page_header'; import { DataSourceTableWithRouter } from '../data_source_table/data_source_table'; -import { ManageDirectQueryDataConnectionsTable } from '../direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table'; +import { ManageDirectQueryDataConnectionsTableWithRouter } from '../direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table'; import { CreateButton } from '../create_button'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { getListBreadcrumbs } from '../breadcrumbs'; @@ -204,15 +204,8 @@ export const DataSourceHomePanel: React.FC = ({ {selectedTabId === 'manageOpensearchDataSources' && featureFlagStatus && ( )} - {selectedTabId === 'manageDirectQueryDataSources' && ( - + {selectedTabId === 'manageDirectQueryDataSources' && featureFlagStatus && ( + )} diff --git a/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap index 9dc785b13b4e..5809054f8293 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap @@ -207,12 +207,12 @@ exports[`DataSourceTable should get datasources successful should render normall ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -227,12 +227,12 @@ exports[`DataSourceTable should get datasources successful should render normall ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], ], @@ -298,10 +298,15 @@ exports[`DataSourceTable should get datasources successful should render normall Object { "dataType": "string", "field": "title", - "name": "Title", + "name": "Data source", "render": [Function], "sortable": [Function], }, + Object { + "field": "type", + "name": "Type", + "truncateText": true, + }, Object { "dataType": "string", "field": "description", @@ -312,6 +317,13 @@ exports[`DataSourceTable should get datasources successful should render normall "sortable": [Function], "truncateText": true, }, + Object { + "align": "right", + "field": "relatedConnections", + "name": "Related connections", + "render": [Function], + "truncateText": true, + }, ] } isSelectable={true} @@ -319,28 +331,36 @@ exports[`DataSourceTable should get datasources successful should render normall items={ Array [ Object { - "description": "test datasource", - "id": "test", - "sort": "test", - "title": "test", + "connectionType": 0, + "description": "test datasource1", + "id": "test1", + "relatedConnections": Array [], + "title": "test1", + "type": "OpenSearch", }, Object { + "connectionType": 0, "description": "test datasource2", "id": "test2", - "sort": "test", + "relatedConnections": Array [], "title": "test", + "type": "OpenSearch", }, Object { + "connectionType": 0, "description": "alpha test datasource", "id": "alpha-test", - "sort": "alpha-test", + "relatedConnections": Array [], "title": "alpha-test", + "type": "OpenSearch", }, Object { + "connectionType": 0, "description": "beta test datasource", "id": "beta-test", - "sort": "beta-test", + "relatedConnections": Array [], "title": "beta-test", + "type": "OpenSearch", }, ] } @@ -370,22 +390,22 @@ exports[`DataSourceTable should get datasources successful should render normall }, }, "compressed": true, - "toolsRight": - - Delete - - - - - - , + "filters": Array [ + Object { + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [ + Object { + "key": "type-option-0", + "name": "OpenSearch", + "value": "OpenSearch", + }, + ], + "type": "field_value_selection", + }, + ], + "toolsLeft": Array [], } } selection={ @@ -418,25 +438,25 @@ exports[`DataSourceTable should get datasources successful should render normall } } compressed={true} - onChange={[Function]} - toolsRight={ - - - Delete - - - - - - + filters={ + Array [ + Object { + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [ + Object { + "key": "type-option-0", + "name": "OpenSearch", + "value": "OpenSearch", + }, + ], + "type": "field_value_selection", + }, + ] } + onChange={[Function]} + toolsLeft={Array []} >
- -
- +
- - + Type + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="field_value_selection_0" + isOpen={false} + ownFocus={true} + panelClassName="euiFilterGroup__popoverPanel" + panelPaddingSize="none" > - - - - -
- + + + +
+
+ + + + +
@@ -631,10 +774,15 @@ exports[`DataSourceTable should get datasources successful should render normall Object { "dataType": "string", "field": "title", - "name": "Title", + "name": "Data source", "render": [Function], "sortable": [Function], }, + Object { + "field": "type", + "name": "Type", + "truncateText": true, + }, Object { "dataType": "string", "field": "description", @@ -645,6 +793,13 @@ exports[`DataSourceTable should get datasources successful should render normall "sortable": [Function], "truncateText": true, }, + Object { + "align": "right", + "field": "relatedConnections", + "name": "Related connections", + "render": [Function], + "truncateText": true, + }, ] } isSelectable={true} @@ -652,28 +807,36 @@ exports[`DataSourceTable should get datasources successful should render normall items={ Array [ Object { + "connectionType": 0, "description": "alpha test datasource", "id": "alpha-test", - "sort": "alpha-test", + "relatedConnections": Array [], "title": "alpha-test", + "type": "OpenSearch", }, Object { + "connectionType": 0, "description": "beta test datasource", "id": "beta-test", - "sort": "beta-test", + "relatedConnections": Array [], "title": "beta-test", + "type": "OpenSearch", }, Object { - "description": "test datasource", - "id": "test", - "sort": "test", - "title": "test", - }, - Object { + "connectionType": 0, "description": "test datasource2", "id": "test2", - "sort": "test", + "relatedConnections": Array [], "title": "test", + "type": "OpenSearch", + }, + Object { + "connectionType": 0, + "description": "test datasource1", + "id": "test1", + "relatedConnections": Array [], + "title": "test1", + "type": "OpenSearch", }, ] } @@ -705,7 +868,7 @@ exports[`DataSourceTable should get datasources successful should render normall "allowNeutralSort": false, "sort": Object { "direction": "asc", - "field": "Title", + "field": "Data source", }, } } @@ -786,13 +949,13 @@ exports[`DataSourceTable should get datasources successful should render normall "isSortAscending": true, "isSorted": true, "key": "_data_s_title_0", - "name": "Title", + "name": "Data source", "onSort": [Function], }, Object { "isSortAscending": undefined, "isSorted": false, - "key": "_data_s_description_1", + "key": "_data_s_description_2", "name": "Description", "onSort": [Function], }, @@ -1015,15 +1178,15 @@ exports[`DataSourceTable should get datasources successful should render normall values={ Object { "description": undefined, - "innerText": "Title", + "innerText": "Data source", } } > - Title + Data source @@ -1045,9 +1208,55 @@ exports[`DataSourceTable should get datasources successful should render normall + + + + + + + Type + + + + + + + + + + + + + + + + Related connections + + + + + + + @@ -1173,7 +1428,7 @@ exports[`DataSourceTable should get datasources successful should render normall key="_data_column_title_alpha-test_0" mobileOptions={ Object { - "header": "Title", + "header": "Data source", "render": undefined, } } @@ -1191,7 +1446,7 @@ exports[`DataSourceTable should get datasources successful should render normall
- Title + Data source
+ +
+ Type +
+
+ + OpenSearch + +
+ +
+ + + +
+ Related connections +
+
+ 0 +
+ +
- Title + Data source
+ +
+ Type +
+
+ + OpenSearch + +
+ +
+ + +
+ + beta test datasource + +
+ +
+ + +
+ Related connections +
+
+ 0 +
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Data source +
+
+ + + +
+ +
+ + +
+ Type +
+
+ + OpenSearch + +
+ +
+ - beta test datasource + test datasource2
+ + +
+ Related connections +
+
+ 0 +
+ +
- Title + Data source
- test + test1 @@ -1582,12 +2201,11 @@ exports[`DataSourceTable should get datasources successful should render normall +
+ Type +
- test datasource + OpenSearch
- - - - - - -
- - -
- -
-
- - -
- -
- Title -
-
- - - + test datasource1 +
- - test datasource2 - + Related connections +
+
+ 0
@@ -2166,12 +2701,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2186,12 +2721,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2206,12 +2741,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2226,7 +2761,17 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", + }, + ], + Array [ + Object { + "pathname": "test1", + }, + ], + Array [ + Object { + "pathname": "test1", }, ], Array [ @@ -2234,6 +2779,11 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource "pathname": "test2", }, ], + Array [ + Object { + "pathname": "beta-test", + }, + ], Array [ Object { "pathname": "alpha-test", @@ -2241,12 +2791,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "beta-test", + "pathname": "alpha-test", }, ], Array [ Object { - "pathname": "test", + "pathname": "beta-test", }, ], Array [ @@ -2254,6 +2804,11 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource "pathname": "test2", }, ], + Array [ + Object { + "pathname": "test1", + }, + ], Array [ Object { "pathname": "alpha-test", @@ -2266,12 +2821,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2286,12 +2841,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2306,12 +2861,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2326,12 +2881,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2346,12 +2901,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2366,12 +2921,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2386,12 +2941,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2406,12 +2961,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2426,12 +2981,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2446,12 +3001,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2466,12 +3021,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2486,12 +3041,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2506,12 +3061,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2526,12 +3081,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2546,12 +3101,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2566,12 +3121,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2586,12 +3141,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2606,12 +3161,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2626,12 +3181,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2646,12 +3201,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2666,12 +3221,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2686,12 +3241,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], Array [ @@ -2706,12 +3261,12 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource ], Array [ Object { - "pathname": "test", + "pathname": "test2", }, ], Array [ Object { - "pathname": "test2", + "pathname": "test1", }, ], ], @@ -3164,6 +3719,22 @@ exports[`DataSourceTable should not manage datasources when canManageDataSource "type": "return", "value": undefined, }, + Object { + "type": "return", + "value": undefined, + }, + Object { + "type": "return", + "value": undefined, + }, + Object { + "type": "return", + "value": undefined, + }, + Object { + "type": "return", + "value": undefined, + }, ], }, "createSubHistory": [MockFunction], diff --git a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx index acb4d5d7d853..6e0a1eef2059 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx @@ -62,7 +62,7 @@ describe('DataSourceTable', () => { describe('should get datasources successful', () => { beforeEach(async () => { spyOn(utils, 'getDataSources').and.returnValue(Promise.resolve(getMappedDataSources)); - spyOn(uiSettings, 'get').and.returnValue('test'); + spyOn(uiSettings, 'get').and.returnValue('test1'); await act(async () => { component = await mount( wrapWithIntl( @@ -88,27 +88,27 @@ describe('DataSourceTable', () => { expect(utils.getDataSources).toHaveBeenCalled(); }); - it('should sort datasources based on description', () => { + it('should sort datasources based on title', () => { expect(component.find(badgeIcon).exists()).toBe(true); expect(component.find(tableIdentifier).exists()).toBe(true); act(() => { - component.find(tableColumnHeaderButtonIdentifier).last().simulate('click'); + component.find(tableColumnHeaderButtonIdentifier).first().simulate('click'); }); component.update(); // @ts-ignore - expect(component.find(tableColumnHeaderIdentifier).last().props().isSorted).toBe(true); + expect(component.find(tableColumnHeaderIdentifier).first().props().isSorted).toBe(true); expect(uiSettings.get).toHaveBeenCalled(); }); - it('should enable delete button when select datasources', () => { - expect(component.find(deleteButtonIdentifier).first().props().disabled).toBe(true); + it('should show delete button when select datasources', () => { + expect(component.find(deleteButtonIdentifier).exists()).toBe(false); act(() => { // @ts-ignore component.find(tableIdentifier).props().selection.onSelectionChange(getMappedDataSources); }); component.update(); - expect(component.find(deleteButtonIdentifier).first().props().disabled).toBe(false); + expect(component.find(deleteButtonIdentifier).exists()).toBe(true); }); it('should delete confirm modal pop up and cancel button work normally', () => { diff --git a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx index d2efbf3441f2..c82ba0c9a522 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx +++ b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx @@ -8,13 +8,13 @@ import { EuiSmallButton, EuiButtonEmpty, EuiConfirmModal, - EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiSpacer, EuiText, + EuiSearchBarProps, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; import { i18n } from '@osd/i18n'; @@ -30,6 +30,7 @@ import { getDataSources, setFirstDataSourceAsDefault, getDefaultDataSourceId, + fetchDataSourceConnections, } from '../utils'; import { LoadingMask } from '../loading_mask'; @@ -49,9 +50,9 @@ const sorting = { export const DataSourceTable = ({ history }: RouteComponentProps) => { const { chrome, - setBreadcrumbs, savedObjects, - notifications: { toasts }, + http, + notifications, uiSettings, application, } = useOpenSearchDashboards().services; @@ -81,7 +82,10 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { setIsLoading(true); getDataSources(savedObjects.client) .then((response: DataSourceTableItem[]) => { - setDataSources(response); + return fetchDataSourceConnections(response, http, notifications); + }) + .then((finalData) => { + setDataSources(finalData); }) .catch(() => { setDataSources([]); @@ -96,32 +100,28 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { }; /* Table search config */ - const renderDeleteButton = () => { - return ( - { - setConfirmDeleteVisible(true); - }} - data-test-subj="deleteDataSourceConnections" - disabled={selectedDataSources.length === 0} - > - Delete {selectedDataSources.length || ''} {selectedDataSources.length ? 'connection' : ''} - {selectedDataSources.length >= 2 ? 's' : ''} - - ); - }; - - const renderToolsRight = () => { - return canManageDataSource ? ( - - {renderDeleteButton()} - - ) : null; - }; + const renderToolsLeft = useCallback(() => { + return selectedDataSources.length > 0 + ? [ + { + setConfirmDeleteVisible(true); + }} + data-test-subj="deleteDataSourceConnections" + > + + , + ] + : []; + }, [selectedDataSources]); - const search = { - toolsRight: renderToolsRight(), + const search: EuiSearchBarProps = { + toolsLeft: canManageDataSource ? renderToolsLeft() : undefined, compressed: true, box: { incremental: true, @@ -129,13 +129,32 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { fields: { title: { type: 'string' } }, }, }, + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('dataSourcesManagement.dataSourcesTable.type', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: Array.from(new Set(dataSources.map(({ type }) => type).filter(Boolean))).map( + (type, index) => ({ + key: `type-option-${index}`, + value: type!, + name: type!, + }) + ), + }, + ], }; /* Table columns */ const columns = [ { field: 'title', - name: 'Title', + name: i18n.translate('dataSourcesManagement.dataSourcesTable.dataSourceField', { + defaultMessage: 'Data source', + }), render: ( name: string, index: { @@ -160,9 +179,18 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { dataType: 'string' as const, sortable: ({ title }: { title: string }) => title, }, + { + field: 'type', + name: i18n.translate('dataSourcesManagement.dataSourcesTable.typeField', { + defaultMessage: 'Type', + }), + truncateText: true, + }, { field: 'description', - name: 'Description', + name: i18n.translate('dataSourcesManagement.dataSourcesTable.descriptionField', { + defaultMessage: 'Description', + }), truncateText: true, mobileOptions: { show: false, @@ -170,6 +198,15 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { dataType: 'string' as const, sortable: ({ description }: { description: string }) => description, }, + { + field: 'relatedConnections', + name: i18n.translate('dataSourcesManagement.dataSourcesTable.relatedConnectionsField', { + defaultMessage: 'Related connections', + }), + align: 'right', + truncateText: true, + render: (relatedConnections: DataSourceTableItem[]) => relatedConnections?.length, + }, ]; /* render delete modal*/ @@ -248,6 +285,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { for (const dataSource of selectedDataSources) { if (getDefaultDataSourceId(uiSettings) === dataSource.id) { await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, true); + break; } } } catch (e) { @@ -272,7 +310,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { /* Toast Handlers */ const handleDisplayToastMessage = ({ id, defaultMessage }: ToastMessageItem) => { - toasts.addDanger( + notifications.toasts.addDanger( i18n.translate(id, { defaultMessage, }) diff --git a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap index 7269ffca7bb9..f1d3efe44faf 100644 --- a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap @@ -1,473 +1,2952 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ManageDirectQueryDataConnectionsTable matches snapshot 1`] = ` - -
+ -
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - + + + + + + + + + + +`; + +exports[`ManageDirectQueryDataConnectionsTable should get direct query connections successful should render normally 1`] = ` + + + +
+ +
+ +
+ + OpenSearch + , + }, + ], + "type": "field_value_selection", + }, + ], + "toolsLeft": Array [], + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "title", + }, + } + } + tableLayout="auto" > -
- - - - -
-
- - - Name - - - - - - Status - - - - - - Actions - - -
-
- Name -
-
+ -
- -
-
- +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ + +
+ + +
+ + + Type + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="field_value_selection_0" + isOpen={false} + ownFocus={true} + panelClassName="euiFilterGroup__popoverPanel" + panelPaddingSize="none" + > +
+
+ + + + + +
+
+
+
+
+
+
+
+
-
-
-
-
+ + - Status -
-
+ +
-
-
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + - -
-
- Active -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + Type + + + + + + + + + + + + Description + + + + + + + + + + + + Related connections + + + + + +
+
+ + No items found + +
+
+
-
-
-
- - - - Delete - - -
-
-
- Name -
-
+ + OpenSearch + , + }, + ], + "type": "field_value_selection", + }, + ] + } + onChange={[Function]} + toolsLeft={Array []} > -
- -
-
- +
+ + OpenSearch + , + }, + ], + "type": "field_value_selection", + }, + ] + } + onChange={[Function]} + query={ + Query { + "ast": _AST { + "_clauses": Array [], + "_indexedClauses": Object { + "field": Object {}, + "group": Array [], + "is": Object {}, + "term": Array [], + }, + }, + "syntax": Object { + "parse": [Function], + "print": [Function], + "printClause": [Function], + }, + "text": "", + } + } + > + +
+ + OpenSearch + , + }, + ], + "type": "field_value_selection", + } + } + index={0} + onChange={[Function]} + query={ + Query { + "ast": _AST { + "_clauses": Array [], + "_indexedClauses": Object { + "field": Object {}, + "group": Array [], + "is": Object {}, + "term": Array [], + }, + }, + "syntax": Object { + "parse": [Function], + "print": [Function], + "printClause": [Function], + }, + "text": "", + } + } + > + + Type + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="field_value_selection_0" + isOpen={false} + ownFocus={true} + panelClassName="euiFilterGroup__popoverPanel" + panelPaddingSize="none" + > +
+
+ + + + + +
+
+
+
+
+
+
+
+
-
-
-
-
+ + - Status -
-
+ +
-
-
- -
-
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + - Inactive -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + Type + + + + + + + + + + + + Description + + + + + + + + + + + + Related connections + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ + + +
+
+
+ Data source +
+ +
+
+ Type +
+
+ + OpenSearch + +
+
+
+ + test datasource1 + +
+
+
+ Related connections +
+
+ 2 +
+
+
-
-
-
-
- - - Accelerate performance - - - - - Delete - - -
-
-
-
-
-
-
-
-
- +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+
-
-
-
- +
-
+
-
+
-
-
- + + + `; diff --git a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/direct_query_table.scss b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/direct_query_table.scss new file mode 100644 index 000000000000..b209996b9e2b --- /dev/null +++ b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/direct_query_table.scss @@ -0,0 +1,15 @@ +.direct-query-expanded-table thead { + display: none; +} + +.direct-query-table [id^="row"][id$="expansion"] > td:first-child > div:first-child { + padding: 0; +} + +.direct-query-expanded-row:first-child > td { + border-top: 0; +} + +.direct-query-expanded-row:last-child > td { + border-bottom: 0; +} diff --git a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.test.tsx b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.test.tsx index c3122d2ac5bf..6f3f8ef959cf 100644 --- a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.test.tsx +++ b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.test.tsx @@ -4,164 +4,158 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; +import * as utils from '../../utils'; +import { mount, ReactWrapper } from 'enzyme'; +import { RouteComponentProps } from 'react-router-dom'; +import { wrapWithIntl } from 'test_utils/enzyme_helpers'; +import { ScopedHistory } from 'opensearch-dashboards/public'; +import { scopedHistoryMock } from '../../../../../../core/public/mocks'; +import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; +import { getMappedDataSources, mockManagementPlugin } from '../../../mocks'; import { ManageDirectQueryDataConnectionsTable } from './manage_direct_query_data_connections_table'; -import { getHideLocalCluster } from '../../utils'; -// Mock dependencies -jest.mock('react-router-dom', () => ({ - useHistory: () => ({ - push: jest.fn(), - }), -})); - -jest.mock('../../../plugin', () => ({ - getRenderCreateAccelerationFlyout: jest.fn(() => jest.fn()), -})); - -jest.mock('../icons/prometheus_logo.svg', () => 'prometheusLogo'); -jest.mock('../icons/s3_logo.svg', () => 's3Logo'); -jest.mock('../integrations/installed_integrations_table', () => ({ - InstallIntegrationFlyout: jest.fn(() =>
MockInstallIntegrationFlyout
), -})); - -jest.mock('../../utils', () => ({ - ...jest.requireActual('../../utils'), - getHideLocalCluster: jest.fn(() => ({ enabled: true })), -})); +const deleteButtonIdentifier = '[data-test-subj="deleteDataSourceConnections"]'; +const tableIdentifier = 'EuiInMemoryTable'; +const confirmModalIdentifier = 'EuiConfirmModal'; describe('ManageDirectQueryDataConnectionsTable', () => { - const mockHttp = { get: jest.fn(), delete: jest.fn() }; - const mockNotifications = { - toasts: { - addSuccess: jest.fn(), - addDanger: jest.fn(), - addWarning: jest.fn(), - }, - }; - const mockSavedObjects = { client: {} }; - const mockUiSettings = {}; - const mockApplication = { navigateToApp: jest.fn() }; - - const defaultProps = { - http: mockHttp, - notifications: mockNotifications, - savedObjects: mockSavedObjects, - uiSettings: mockUiSettings, - featureFlagStatus: false, - application: mockApplication, + const mockedContext = { + ...mockManagementPlugin.createDataSourceManagementContext(), + application: { capabilities: { dataSource: { canManage: true } } }, }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('renders data connections', async () => { - mockHttp.get.mockResolvedValue([ - { name: 'connection1', connector: 'PROMETHEUS', status: 'ACTIVE' }, - { name: 'connection2', connector: 'S3GLUE', status: 'INACTIVE' }, - ]); - - render(); - await waitFor(() => expect(screen.getByText('connection1')).toBeInTheDocument()); - expect(screen.getByText('connection2')).toBeInTheDocument(); - }); - - test('handles search input change', async () => { - mockHttp.get.mockResolvedValue([ - { name: 'connection1', connector: 'PROMETHEUS', status: 'ACTIVE' }, - { name: 'connection2', connector: 'S3GLUE', status: 'INACTIVE' }, - ]); - - render(); - await waitFor(() => expect(screen.getByText('connection1')).toBeInTheDocument()); - - const searchInput = screen.getByPlaceholderText('Search...'); - fireEvent.change(searchInput, { target: { value: 'connection2' } }); - - expect(screen.queryByText('connection1')).not.toBeInTheDocument(); - expect(screen.getByText('connection2')).toBeInTheDocument(); - }); - - test('displays error on failed fetch', async () => { - mockHttp.get.mockRejectedValue(new Error('Fetch error')); - - render(); - - await waitFor(() => - expect(mockNotifications.toasts.addDanger).toHaveBeenCalledWith( - 'Could not fetch data sources' - ) - ); - }); - - test('matches snapshot', async () => { - mockHttp.get.mockResolvedValue([ - { name: 'connection1', connector: 'PROMETHEUS', status: 'ACTIVE' }, - { name: 'connection2', connector: 'S3GLUE', status: 'INACTIVE' }, - ]); - - const { asFragment } = render(); - await waitFor(() => expect(screen.getByText('connection1')).toBeInTheDocument()); - expect(asFragment()).toMatchSnapshot(); - }); - - // Conditional rendering tests - test('renders no connections message when there are no connections', async () => { - mockHttp.get.mockResolvedValue([]); - - render(); - await waitFor(() => expect(screen.getByText('No items found')).toBeInTheDocument()); - }); - - test('renders error message when fetch fails', async () => { - mockHttp.get.mockRejectedValue(new Error('Fetch error')); - - render(); - await waitFor(() => - expect(mockNotifications.toasts.addDanger).toHaveBeenCalledWith( - 'Could not fetch data sources' - ) - ); + const uiSettings = mockedContext.uiSettings; + let component: ReactWrapper, React.Component<{}, {}, any>>; + const history = (scopedHistoryMock.create() as unknown) as ScopedHistory; + describe('should get direct query connections failed', () => { + beforeEach(async () => { + spyOn(utils, 'getDataSources').and.returnValue(Promise.reject()); + await act(async () => { + component = await mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + }); + component.update(); + }); + test('should render empty table', () => { + expect(component).toMatchSnapshot(); + }); }); - test('displays loading indicator while fetching data', async () => { - mockHttp.get.mockImplementation( - () => - new Promise((resolve) => { - setTimeout( - () => - resolve([ - { name: 'connection1', connector: 'PROMETHEUS', status: 'ACTIVE' }, - { name: 'connection2', connector: 'S3GLUE', status: 'INACTIVE' }, - ]), - 1000 - ); - }) - ); + describe('should get direct query connections successful', () => { + beforeEach(async () => { + spyOn(utils, 'getDataSources').and.returnValue(Promise.resolve(getMappedDataSources)); + spyOn(utils, 'fetchDataSourceConnections').and.returnValue( + Promise.resolve(getMappedDataSources) + ); + spyOn(uiSettings, 'get').and.returnValue('test1'); + await act(async () => { + component = await mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + }); + component.update(); + }); - render(); - expect(screen.getByText('Loading direct query data connections...')).toBeInTheDocument(); + it('should render normally', () => { + expect(component).toMatchSnapshot(); + expect(utils.getDataSources).toHaveBeenCalled(); + }); - await waitFor(() => expect(screen.getByText('connection1')).toBeInTheDocument()); - expect(screen.queryByText('Loading direct query data connections...')).not.toBeInTheDocument(); - }); + it('should show delete button when select datasources', () => { + expect(component.find(deleteButtonIdentifier).exists()).toBe(false); - test('renders DataSourceSelector with hideLocalCluster enabled', async () => { - const newProps = { - ...defaultProps, - featureFlagStatus: true, - }; + act(() => { + // @ts-ignore + component.find(tableIdentifier).props().selection.onSelectionChange(getMappedDataSources); + }); + component.update(); + expect(component.find(deleteButtonIdentifier).exists()).toBe(true); + }); - render(); + it('should delete confirm modal pop up and cancel button work normally', () => { + act(() => { + // @ts-ignore + component.find(tableIdentifier).props().selection.onSelectionChange(getMappedDataSources); + }); + component.update(); + component.find(deleteButtonIdentifier).first().simulate('click'); + // test if modal pop up when click the delete button + expect(component.find(confirmModalIdentifier).exists()).toBe(true); + + act(() => { + // @ts-ignore + component.find(confirmModalIdentifier).first().props().onCancel(); + }); + component.update(); + expect(component.find(confirmModalIdentifier).exists()).toBe(false); + }); - await waitFor(() => { - const dataSourceSelector = screen.getByTestId('dataSourceSelectorComboBox'); - expect(dataSourceSelector).toBeInTheDocument(); + it('should delete confirm modal confirm button work normally', async () => { + spyOn(utils, 'deleteMultipleDataSources').and.returnValue(Promise.resolve({})); + spyOn(utils, 'setFirstDataSourceAsDefault').and.returnValue({}); + act(() => { + // @ts-ignore + component.find(tableIdentifier).props().selection.onSelectionChange(getMappedDataSources); + }); + component.update(); + component.find(deleteButtonIdentifier).first().simulate('click'); + expect(component.find(confirmModalIdentifier).exists()).toBe(true); + + await act(async () => { + // @ts-ignore + await component.find(confirmModalIdentifier).first().props().onConfirm(); + }); + component.update(); + expect(component.find(confirmModalIdentifier).exists()).toBe(false); + expect(utils.setFirstDataSourceAsDefault).toHaveBeenCalled(); }); - // Verify that the hideLocalCluster prop is passed correctly - expect(getHideLocalCluster).toHaveBeenCalled(); - expect(getHideLocalCluster().enabled).toBe(true); + it('should delete datasources & fail', async () => { + spyOn(utils, 'deleteMultipleDataSources').and.returnValue(Promise.reject({})); + spyOn(utils, 'setFirstDataSourceAsDefault').and.returnValue({}); + act(() => { + // @ts-ignore + component.find(tableIdentifier).props().selection.onSelectionChange(getMappedDataSources); + }); + + component.update(); + component.find(deleteButtonIdentifier).first().simulate('click'); + expect(component.find(confirmModalIdentifier).exists()).toBe(true); + + await act(async () => { + // @ts-ignore + await component.find(confirmModalIdentifier).props().onConfirm(); + }); + component.update(); + expect(utils.deleteMultipleDataSources).toHaveBeenCalled(); + expect(utils.setFirstDataSourceAsDefault).not.toHaveBeenCalled(); + // @ts-ignore + expect(component.find(confirmModalIdentifier).exists()).toBe(false); + }); }); }); diff --git a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx index 9d38347f3dac..1c6fe3732f2e 100644 --- a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx +++ b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx @@ -3,145 +3,222 @@ * SPDX-License-Identifier: Apache-2.0 */ +import './direct_query_table.scss'; import { EuiFlexGroup, EuiFlexItem, - EuiHealth, - EuiIcon, + EuiButtonIcon, EuiInMemoryTable, - EuiLink, - EuiOverlayMask, + EuiSmallButton, + EuiConfirmModal, EuiPageBody, EuiSpacer, EuiTableFieldDataColumnType, - EuiCompressedFieldSearch, + EuiSearchBarProps, EuiLoadingSpinner, EuiText, + LEFT_ALIGNMENT, + EuiButtonEmpty, } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from 'react-intl'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; import { - ApplicationStart, - HttpStart, - IUiSettingsClient, - NotificationsStart, - SavedObjectsStart, -} from 'opensearch-dashboards/public'; -import { useHistory } from 'react-router-dom'; -import { - DirectQueryDatasourceDetails, - DirectQueryDatasourceStatus, - DirectQueryDatasourceType, + DataSourceConnectionType, + DataSourceManagementContext, + DataSourceTableItem, } from '../../../types'; -import { DeleteModal } from './direct_query_data_source_delete_modal'; -import PrometheusLogo from '../icons/prometheus_logo.svg'; -import S3Logo from '../icons/s3_logo.svg'; -import { DataSourceSelector } from '../../data_source_selector'; -import { DataSourceOption } from '../../data_source_menu/types'; -import { DATACONNECTIONS_BASE, observabilityMetricsID } from '../../../constants'; -import { getRenderCreateAccelerationFlyout } from '../../../plugin'; -import { InstallIntegrationFlyout } from '../integrations/installed_integrations_table'; -import { redirectToExplorerS3 } from '../associated_object_management/utils/associated_objects_tab_utils'; -import { isPluginInstalled, getHideLocalCluster } from '../../utils'; - -interface DataConnection { - connectionType: DirectQueryDatasourceType; - name: string; - dsStatus: DirectQueryDatasourceStatus; -} - -interface ManageDirectQueryDataConnectionsTableProps { - http: HttpStart; - notifications: NotificationsStart; - savedObjects: SavedObjectsStart; - uiSettings: IUiSettingsClient; - featureFlagStatus: boolean; - application: ApplicationStart; -} - -// Custom truncate function -const truncate = (text: string, length: number) => { - if (text.length <= length) return text; - return text.substring(0, length) + '...'; -}; +import { + isPluginInstalled, + fetchDataSourceConnections, + getDataSources, + deleteMultipleDataSources, + getDefaultDataSourceId, + setFirstDataSourceAsDefault, +} from '../../utils'; +import { LoadingMask } from '../../loading_mask'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -export const ManageDirectQueryDataConnectionsTable: React.FC = ({ - http, - notifications, - savedObjects, - uiSettings, - featureFlagStatus, - application, -}) => { +export const ManageDirectQueryDataConnectionsTable = ({ history }: RouteComponentProps) => { + const { savedObjects, http, notifications, uiSettings } = useOpenSearchDashboards< + DataSourceManagementContext + >().services; const [observabilityDashboardsExists, setObservabilityDashboardsExists] = useState(false); const [showIntegrationsFlyout, setShowIntegrationsFlyout] = useState(false); const [integrationsFlyout, setIntegrationsFlyout] = useState(null); - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [isModalVisible, setIsModalVisible] = useState(false); - const [modalLayout, setModalLayout] = useState(); - const [selectedConnection, setSelectedConnection] = useState(undefined); - const [selectedDataSourceId, setSelectedDataSourceId] = useState(undefined); - const [searchText, setSearchText] = useState(''); + const [isDeleting, setIsDeleting] = React.useState(false); + const [selectedDataSources, setSelectedDataSources] = useState([]); + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record + >({}); const [isLoading, setIsLoading] = useState(false); - const history = useHistory(); - const fetchDataSources = useCallback(() => { - if (featureFlagStatus && selectedDataSourceId === undefined) return; + /* Table selection handlers */ + const onSelectionChange = (selected: DataSourceTableItem[]) => { + setSelectedDataSources(selected); + }; - const endpoint = - featureFlagStatus && selectedDataSourceId !== undefined - ? `${DATACONNECTIONS_BASE}/dataSourceMDSId=${selectedDataSourceId}` - : `${DATACONNECTIONS_BASE}`; + const selection = { + onSelectionChange, + }; - setIsLoading(true); + const setDefaultDataSource = async () => { + try { + for (const dataSource of selectedDataSources) { + if (getDefaultDataSourceId(uiSettings) === dataSource.id) { + await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, true); + break; + } + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('dataSourcesManagement.directQueryTable.setDefaultDataSourceFailMsg', { + defaultMessage: + 'No default data source has been set. Please select a new default data source.', + }) + ); + } finally { + setIsDeleting(false); + } + }; - http - .get(endpoint) - .then((res: DirectQueryDatasourceDetails[]) => { - const dataConnections = res.map((dataConnection: DirectQueryDatasourceDetails) => ({ - name: dataConnection.name, - connectionType: dataConnection.connector, - dsStatus: dataConnection.status, - })); - setData(dataConnections); + /* Delete selected data sources*/ + const onClickDelete = () => { + setIsDeleting(true); + + deleteMultipleDataSources(savedObjects.client, selectedDataSources) + .then(() => { + setSelectedDataSources([]); + // Fetch data sources + fetchDataSources(); + setIsModalVisible(false); + // Check if default data source is deleted or not. + // if yes, then set the first existing datasource as default datasource. + setDefaultDataSource(); }) - .catch((err) => { - notifications.toasts.addDanger('Could not fetch data sources'); + .catch(() => { + notifications.toasts.addDanger( + i18n.translate('dataSourcesManagement.directQueryTable.deleteDataSourceFailMsg', { + defaultMessage: + 'An error occurred while attempting to delete the selected data sources. Please try it again', + }) + ); }) .finally(() => { - setIsLoading(false); + setIsDeleting(false); }); - }, [http, notifications.toasts, selectedDataSourceId, featureFlagStatus]); + }; + + /* render delete modal*/ + const tableRenderDeleteModal = () => { + return isModalVisible ? ( + { + setIsModalVisible(false); + }} + onConfirm={() => { + setIsModalVisible(false); + onClickDelete(); + }} + cancelButtonText={i18n.translate('dataSourcesManagement.directQueryTable.cancel', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('dataSourcesManagement.directQueryTable.delete', { + defaultMessage: 'Delete', + })} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

+ +

+

+ +

+

+ +

+
+ ) : null; + }; - const deleteDataSources = useCallback( - (connectionName: string | undefined) => { - if (!connectionName || (featureFlagStatus && selectedDataSourceId === undefined)) return; + const renderToolsLeft = useCallback(() => { + return selectedDataSources.length > 0 + ? [ + setIsModalVisible(true)} + data-test-subj="deleteDataSourceConnections" + > + + , + ] + : []; + }, [selectedDataSources]); - const endpoint = - featureFlagStatus && selectedDataSourceId !== undefined - ? `${DATACONNECTIONS_BASE}/${connectionName}/dataSourceMDSId=${selectedDataSourceId}` - : `${DATACONNECTIONS_BASE}/${connectionName}`; + const toggleDetails = (item: DataSourceTableItem) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item.id]) { + delete itemIdToExpandedRowMapValues[item.id]; + } else { + itemIdToExpandedRowMapValues[item.id] = ( + + ); + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; - setIsLoading(true); + const fetchDataSources = useCallback(() => { + setIsLoading(true); - http - .delete(endpoint) - .then(() => { - notifications.toasts.addSuccess(`Data connection ${connectionName} deleted successfully`); - setData(data.filter((connection) => connection.name !== connectionName)); - }) - .catch((err) => { - notifications.toasts.addDanger( - `Data connection ${connectionName} not deleted. See output for more details.` - ); - }) - .finally(() => { - setIsLoading(false); - setSelectedConnection(undefined); // Clear the selected connection after deletion - }); - }, - [http, notifications.toasts, selectedDataSourceId, featureFlagStatus, data] - ); + getDataSources(savedObjects.client) + .then((response: DataSourceTableItem[]) => { + return fetchDataSourceConnections(response, http, notifications); + }) + .then((finalData) => { + setData(finalData.filter((item) => item.relatedConnections?.length > 0)); + }) + .catch(() => { + setData([]); + notifications.toasts.addDanger( + i18n.translate('dataSourcesManagement.directQueryTable.fetchDataSources', { + defaultMessage: 'Could not fetch data sources', + }) + ); + }) + .finally(() => { + setIsLoading(false); + }); + }, [http, savedObjects, notifications]); useEffect(() => { fetchDataSources(); @@ -151,190 +228,103 @@ export const ManageDirectQueryDataConnectionsTable: React.FC { - const dataSourceId = e[0] ? e[0].id : undefined; - setSelectedDataSourceId(dataSourceId); - }; - - const displayDeleteModal = (connectionName: string) => { - setSelectedConnection(connectionName); - setModalLayout( - { - setIsModalVisible(false); - deleteDataSources(connectionName); - }} - onCancel={() => { - setIsModalVisible(false); - setSelectedConnection(undefined); // Clear the selected connection if cancel is clicked - }} - title={`Delete ${connectionName}`} - message={`Are you sure you want to delete ${connectionName}?`} - /> - ); - setIsModalVisible(true); - }; - - const renderCreateAccelerationFlyout = getRenderCreateAccelerationFlyout(); - - const actions = [ - { - name: (datasource: DataConnection) => - `Query in ${ - datasource.connectionType === 'PROMETHEUS' ? 'Metrics Analytics' : 'Observability Logs' - }`, - isPrimary: true, - icon: 'discoverApp', - type: 'icon', - available: (datasource: DataConnection) => observabilityDashboardsExists, - onClick: (datasource: DataConnection) => { - if (datasource.connectionType === 'PROMETHEUS') { - application!.navigateToApp(observabilityMetricsID); - } else if (datasource.connectionType === 'S3GLUE') { - redirectToExplorerS3(datasource.name, application); - } - }, - 'data-test-subj': 'action-query', - }, + const tableColumns = [ { - name: 'Accelerate performance', - isPrimary: false, - icon: 'bolt', - type: 'icon', - available: (datasource: DataConnection) => datasource.connectionType !== 'PROMETHEUS', - onClick: (datasource: DataConnection) => { - renderCreateAccelerationFlyout({ - dataSourceName: datasource.name, - dataSourceMDSId: selectedDataSourceId ?? '', - }); - }, - 'data-test-subj': 'action-accelerate', + align: LEFT_ALIGNMENT, + width: '40px', + isExpander: true, + render: (item: DataSourceTableItem) => + item?.relatedConnections?.length ? ( + toggleDetails(item)} + aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + /> + ) : null, }, { - name: 'Integrate data', - isPrimary: false, - icon: 'integrationGeneral', - type: 'icon', - available: (datasource: DataConnection) => - !featureFlagStatus && - observabilityDashboardsExists && - datasource.connectionType !== 'PROMETHEUS', - onClick: (datasource: DataConnection) => { - setIntegrationsFlyout( - setShowIntegrationsFlyout(false)} - datasourceType={datasource.connectionType} - datasourceName={datasource.name} - http={http} - /> + width: '25%', + field: 'title', + name: i18n.translate('dataSourcesManagement.directQueryTable.dataSourceField', { + defaultMessage: 'Data source', + }), + sortable: true, + truncateText: true, + render: (name: string, record: DataSourceTableItem) => { + const path = + record.connectionType === DataSourceConnectionType.OpenSearchConnection + ? record.id + : `manage/${name}?dataSourceMDSId=${record.parentId}`; + + const indentStyle = + record.connectionType !== DataSourceConnectionType.OpenSearchConnection + ? { marginLeft: '20px' } + : {}; + return ( + + {name} + ); - setShowIntegrationsFlyout(true); }, - 'data-test-subj': 'action-integrate', - }, - { - name: 'Delete', - description: 'Delete this data source', - icon: 'trash', - color: 'danger', - type: 'icon', - onClick: (datasource: DataConnection) => displayDeleteModal(datasource.name), - isPrimary: false, - 'data-test-subj': 'action-delete', }, - ]; - - const icon = (record: DataConnection) => { - switch (record.connectionType) { - case 'S3GLUE': - return ; - case 'PROMETHEUS': - return ; - default: - return <>; - } - }; - - const tableColumns = [ { - field: 'name', - name: 'Name', - sortable: true, + width: '15%', + field: 'type', + name: i18n.translate('dataSourcesManagement.directQueryTable.typeField', { + defaultMessage: 'Type', + }), truncateText: true, - render: (value, record: DataConnection) => ( - - {icon(record)} - - - history.push(`/manage/${record.name}?dataSourceMDSId=${selectedDataSourceId ?? ''}`) - } - > - {truncate(record.name, 100)} - - - - ), }, { - field: 'status', - name: 'Status', - sortable: true, + width: '35%', + field: 'description', + name: i18n.translate('dataSourcesManagement.directQueryTable.descriptionField', { + defaultMessage: 'Description', + }), truncateText: true, - render: (value, record: DataConnection) => - record.dsStatus === 'ACTIVE' ? ( - Active - ) : ( - Inactive - ), + mobileOptions: { + show: false, + }, }, { - field: 'actions', - name: 'Actions', - actions, + field: 'relatedConnections', + name: i18n.translate('dataSourcesManagement.directQueryTable.relatedConnectionsField', { + defaultMessage: 'Related connections', + }), + align: 'right', + truncateText: true, + render: (relatedConnections: DataSourceTableItem[]) => relatedConnections?.length, }, - ] as Array>; + ] as Array>; - const customSearchBar = ( - - {featureFlagStatus && ( - - - - )} - - setSearchText(e.target.value)} - isClearable - fullWidth={true} - /> - - - ); - - const entries = data.filter((dataconnection) => - dataconnection.name.toLowerCase().includes(searchText.toLowerCase()) - ); + const customSearchBar: EuiSearchBarProps = { + toolsLeft: renderToolsLeft(), + box: { + incremental: true, + }, + compressed: true, + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('dataSourcesManagement.directQueryTable.type', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: Array.from(new Set(data.map(({ type }) => type).filter(Boolean))).map((type) => ({ + value: type!, + name: type!, + view: <>{type}, + })), + }, + ], + }; return ( + {tableRenderDeleteModal()} - {customSearchBar} - {isLoading ? (
@@ -343,8 +333,16 @@ export const ManageDirectQueryDataConnectionsTable: React.FC ) : ( )} + {isDeleting ? : null} - {isModalVisible && modalLayout} {showIntegrationsFlyout && integrationsFlyout} ); }; + +export const ManageDirectQueryDataConnectionsTableWithRouter = withRouter( + ManageDirectQueryDataConnectionsTable +); diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 848244fc67e8..127a69e69d5e 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -12,12 +12,15 @@ import { ApplicationStart, CoreStart, NotificationsStart, + HttpSetup, } from 'src/core/public'; import { deepFreeze } from '@osd/std'; import uuid from 'uuid'; import { DataSourceAttributes, + DataSourceConnectionType, DataSourceTableItem, + DirectQueryDatasourceDetails, defaultAuthType, noAuthCredentialAuthMethod, } from '../types'; @@ -39,12 +42,88 @@ import { defaultDataSourceSelection, } from '../service/data_source_selection_service'; import { DataSourceError } from '../types'; +import { DATACONNECTIONS_BASE } from '../constants'; + +export const getDirectQueryConnections = async (dataSourceId: string, http: HttpSetup) => { + const endpoint = `${DATACONNECTIONS_BASE}/dataSourceMDSId=${dataSourceId}`; + const res = await http.get(endpoint); + if (!Array.isArray(res)) { + throw new Error('Unexpected response format: expected an array of direct query connections.'); + } + const directQueryConnections: DataSourceTableItem[] = res.map( + (dataConnection: DirectQueryDatasourceDetails) => ({ + id: `${dataSourceId}-${dataConnection.name}`, + title: dataConnection.name, + type: + { + S3GLUE: 'Amazon S3', + PROMETHEUS: 'Prometheus', + }[dataConnection.connector] || dataConnection.connector, + connectionType: DataSourceConnectionType.DirectQueryConnection, + description: dataConnection.description, + parentId: dataSourceId, + }) + ); + return directQueryConnections; +}; + +export const mergeDataSourcesWithConnections = ( + dataSources: DataSourceTableItem[], + directQueryConnections: DataSourceTableItem[] +): DataSourceTableItem[] => { + const dataSourcesList: DataSourceTableItem[] = []; + dataSources.forEach((ds) => { + const relatedConnections = directQueryConnections.filter( + (directQueryConnection) => directQueryConnection.parentId === ds.id + ); + + dataSourcesList.push({ + id: ds.id, + type: ds.type, + connectionType: DataSourceConnectionType.OpenSearchConnection, + title: ds.title, + description: ds.description, + relatedConnections, + }); + }); + + return dataSourcesList; +}; + +export const fetchDataSourceConnections = async ( + dataSources: DataSourceTableItem[], + http: HttpSetup | undefined, + notifications: NotificationsStart | undefined +) => { + try { + const directQueryConnectionsPromises = dataSources.map((ds) => + getDirectQueryConnections(ds.id, http!).catch(() => []) + ); + const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); + const directQueryConnections = directQueryConnectionsResult.flat(); + return mergeDataSourcesWithConnections(dataSources, directQueryConnections); + } catch (error) { + notifications?.toasts.addDanger( + i18n.translate('dataSource.fetchDataSourceConnections', { + defaultMessage: 'Cannot fetch data sources', + }) + ); + return []; + } +}; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient .find({ type: 'data-source', - fields: ['id', 'description', 'title', 'dataSourceVersion', 'installedPlugins'], + fields: [ + 'id', + 'description', + 'title', + 'dataSourceVersion', + 'dataSourceEngineType', + 'installedPlugins', + ], perPage: 10000, }) .then( @@ -54,6 +133,7 @@ export async function getDataSources(savedObjectsClient: SavedObjectsClientContr const title = source.get('title'); const description = source.get('description'); const datasourceversion = source.get('dataSourceVersion'); + const type = source.get('dataSourceEngineType'); const installedplugins = source.get('installedPlugins'); return { @@ -62,6 +142,7 @@ export async function getDataSources(savedObjectsClient: SavedObjectsClientContr description, sort: `${title}`, datasourceversion, + type, installedplugins, }; }) || [] diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index 9c346d0af5e2..aa77ebece0a0 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -8,7 +8,7 @@ import { throwError } from 'rxjs'; import { HttpStart, SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { IUiSettingsClient } from 'src/core/public'; import { DataSourcePluginSetup } from 'src/plugins/data_source/public'; -import { AuthType, DataSourceAttributes } from './types'; +import { AuthType, DataSourceAttributes, DataSourceTableItem } from './types'; import { coreMock } from '../../../core/public/mocks'; import { DataSourceManagementPlugin, @@ -233,29 +233,54 @@ export const existingDatasourceNamesList = [ 'dup20', ]; +export const directQueryConnections: DataSourceTableItem[] = [ + { + id: 'DQ1', + type: 'Amazon S3', + title: 'DQ1', + parentId: 'test1', + description: 'DQ1 test resource', + }, + { + id: 'DQ2', + type: 'Amazon S3', + title: 'DQ2', + parentId: 'test1', + description: 'DQ2 test resource', + }, +]; + export const getMappedDataSources = [ { - id: 'test', - description: 'test datasource', - title: 'test', - sort: 'test', + id: 'test1', + type: 'OpenSearch', + title: 'test1', + connectionType: 'OpenSearchConnection', + description: 'test datasource1', + relatedConnections: directQueryConnections, }, { id: 'test2', + type: 'OpenSearch', description: 'test datasource2', title: 'test', + connectionType: 'OpenSearchConnection', sort: 'test', }, { id: 'alpha-test', + type: 'OpenSearch', description: 'alpha test datasource', title: 'alpha-test', + connectionType: 'OpenSearchConnection', sort: 'alpha-test', }, { id: 'beta-test', + type: 'OpenSearch', description: 'beta test datasource', title: 'beta-test', + connectionType: 'OpenSearchConnection', sort: 'beta-test', }, ]; diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index 2a54f67aea5d..39a653ec9308 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -41,11 +41,20 @@ export interface DataSourceManagementContext { workspaces: WorkspacesStart; } +export enum DataSourceConnectionType { + OpenSearchConnection, + DirectQueryConnection, +} + export interface DataSourceTableItem { id: string; + type?: string; title: string; - description: string; - sort: string; + parentId?: string; + connectionType?: DataSourceConnectionType; + description?: string; + sort?: string; + relatedConnections?: DataSourceTableItem[]; } export interface ToastMessageItem { @@ -181,15 +190,3 @@ export interface PermissionsConfigurationProps { layout: 'horizontal' | 'vertical'; hasSecurityAccess: boolean; } - -export interface DirectQueryDatasourceDetails { - allowedRoles: string[]; - name: string; - connector: DirectQueryDatasourceType; - description: string; - properties: S3GlueProperties | PrometheusProperties; - status: DirectQueryDatasourceStatus; -} -export interface PrometheusProperties { - 'prometheus.uri': string; -}