+
+

+ Manage existing data connections +

+
+
+
+ Manage already created data source connections. +
+
+
+
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + Name + + + + + + Connection Status + + + + + + Actions + + +
+
+ Name +
+
+
+ +
+
+
+ Connection Status +
+
+
+
+
+
+ Actions +
+
+ +
+
+
+ Name +
+
+
+ +
+
+
+ Connection Status +
+
+
+
+
+
+ Actions +
+
+ +
+
+
+ Name +
+
+
+ +
+
+
+ Connection Status +
+
+
+
+
+
+ Actions +
+
+ +
+
+
+ Name +
+
+
+ +
+
+
+ Connection Status +
+
+
+
+
+
+ Actions +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/data_connections/components/__tests__/datasource.test.tsx b/public/components/data_connections/components/__tests__/datasource.test.tsx new file mode 100644 index 0000000000..8294e05989 --- /dev/null +++ b/public/components/data_connections/components/__tests__/datasource.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { act, waitFor } from '@testing-library/react'; +import React from 'react'; +import { DataConnectionsDescription } from '../manage_datasource_description'; +import { ManageDatasourcesTable } from '../manage_datasource_table'; +import { describeDatasource, showDatasourceData } from './testing_constants'; +import { DataSource } from '../datasource'; +import ReactDOM from 'react-dom'; + +describe('Datasource Page test', () => { + configure({ adapter: new Adapter() }); + + it('Renders datasource page with data', async () => { + const http = { + get: jest.fn().mockResolvedValue(describeDatasource), + }; + const pplService = { + fetch: jest.fn(), + }; + const mockChrome = { + setBreadcrumbs: jest.fn(), + }; + const wrapper = mount(); + const container = document.createElement('div'); + await act(() => { + ReactDOM.render( + , + container + ); + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/public/components/data_connections/components/__tests__/manage_datasource_description.test.tsx b/public/components/data_connections/components/__tests__/manage_datasource_description.test.tsx new file mode 100644 index 0000000000..e6380b0d51 --- /dev/null +++ b/public/components/data_connections/components/__tests__/manage_datasource_description.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { DataConnectionsDescription } from '../manage_datasource_description'; + +describe('Manage Datasource Description test', () => { + configure({ adapter: new Adapter() }); + + it('Renders manage datasource description', async () => { + const wrapper = mount(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/data_connections/components/__tests__/manage_datasource_table.test.tsx b/public/components/data_connections/components/__tests__/manage_datasource_table.test.tsx new file mode 100644 index 0000000000..599a7a235e --- /dev/null +++ b/public/components/data_connections/components/__tests__/manage_datasource_table.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { act } from '@testing-library/react'; +import React from 'react'; +import { ManageDatasourcesTable } from '../manage_datasource_table'; +import { showDatasourceData } from './testing_constants'; +import ReactDOM from 'react-dom'; + +describe('Manage Datasource Table test', () => { + configure({ adapter: new Adapter() }); + + it('Renders manage datasource table with data', async () => { + const http = { + get: jest.fn().mockResolvedValue(showDatasourceData), + }; + const pplService = { + fetch: jest.fn(), + }; + const mockChrome = { + setBreadcrumbs: jest.fn(), + }; + const container = document.createElement('div'); + await act(() => { + ReactDOM.render( + , + container + ); + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/public/components/data_connections/components/__tests__/testing_constants.ts b/public/components/data_connections/components/__tests__/testing_constants.ts new file mode 100644 index 0000000000..5cad339575 --- /dev/null +++ b/public/components/data_connections/components/__tests__/testing_constants.ts @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const showDatasourceData = [ + { + name: 'my_spark3', + connector: 'SPARK', + allowedRoles: [], + properties: { + 'spark.connector': 'emr', + 'spark.datasource.flint.host': '0.0.0.0', + 'spark.datasource.flint.integration': + 'https://aws.oss.sonatype.org/content/repositories/snapshots/org/opensearch/opensearch-spark-standalone_2.12/0.1.0-SNAPSHOT/opensearch-spark-standalone_2.12-0.1.0-20230731.182705-3.jar', + 'spark.datasource.flint.port': '9200', + 'spark.datasource.flint.scheme': 'http', + 'emr.cluster': 'j-3UNQLT1MPBGLG', + }, + }, + { + name: 'my_spark4', + connector: 'SPARK', + allowedRoles: [], + properties: { + 'spark.connector': 'emr', + 'spark.datasource.flint.host': '15.248.1.68', + 'spark.datasource.flint.integration': + 'https://aws.oss.sonatype.org/content/repositories/snapshots/org/opensearch/opensearch-spark-standalone_2.12/0.1.0-SNAPSHOT/opensearch-spark-standalone_2.12-0.1.0-20230731.182705-3.jar', + 'spark.datasource.flint.port': '9200', + 'spark.datasource.flint.scheme': 'http', + 'emr.cluster': 'j-3UNQLT1MPBGLG', + }, + }, + { + name: 'my_spark', + connector: 'SPARK', + allowedRoles: [], + properties: { + 'spark.connector': 'emr', + 'spark.datasource.flint.host': '0.0.0.0', + 'spark.datasource.flint.port': '9200', + 'spark.datasource.flint.scheme': 'http', + 'spark.datasource.flint.region': 'xxx', + 'emr.cluster': 'xxx', + }, + }, + { + name: 'my_spark2', + connector: 'SPARK', + allowedRoles: [], + properties: { + 'spark.connector': 'emr', + 'spark.datasource.flint.host': '0.0.0.0', + 'spark.datasource.flint.port': '9200', + 'spark.datasource.flint.scheme': 'http', + 'emr.cluster': 'j-3UNQLT1MPBGLG', + }, + }, +]; + +export const describeDatasource = { + name: 'my_spark3', + connector: 'SPARK', + allowedRoles: [], + properties: { + 'spark.connector': 'emr', + 'spark.datasource.flint.host': '0.0.0.0', + 'spark.datasource.flint.integration': + 'https://aws.oss.sonatype.org/content/repositories/snapshots/org/opensearch/opensearch-spark-standalone_2.12/0.1.0-SNAPSHOT/opensearch-spark-standalone_2.12-0.1.0-20230731.182705-3.jar', + 'spark.datasource.flint.port': '9200', + 'spark.datasource.flint.scheme': 'http', + 'emr.cluster': 'j-3UNQLT1MPBGLG', + }, +}; diff --git a/public/components/data_connections/components/datasource.tsx b/public/components/data_connections/components/datasource.tsx new file mode 100644 index 0000000000..19c6b9fbec --- /dev/null +++ b/public/components/data_connections/components/datasource.tsx @@ -0,0 +1,178 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiSpacer, + EuiTitle, + EuiText, + EuiPanel, + EuiPageHeader, + EuiPageHeaderSection, + EuiAccordion, + EuiIcon, + EuiCard, + EuiTab, + EuiTabs, +} from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { DATASOURCES_BASE } from '../../../../common/constants/shared'; + +interface DatasourceDetails { + allowedRoles: string[]; + name: string; + cluster: string; +} + +export function DataSource(props: any) { + const { dataSource, pplService, http } = props; + const [datasourceDetails, setDatasourceDetails] = useState({ + allowedRoles: [], + name: '', + cluster: '', + }); + + useEffect(() => { + http.get(`${DATASOURCES_BASE}/${dataSource}`).then((data) => + setDatasourceDetails({ + allowedRoles: data.allowedRoles, + name: data.name, + cluster: data.properties['emr.cluster'], + }) + ); + }, []); + + const tabs = [ + { + id: 'data', + name: 'Data', + disabled: false, + }, + { + id: 'access_control', + name: 'Access control', + disabled: false, + }, + { + id: 'connection_configuration', + name: 'Connection configuration', + disabled: false, + }, + ]; + + const [selectedTabId, setSelectedTabId] = useState('data'); + + const onSelectedTabChanged = (id) => { + setSelectedTabId(id); + }; + + const renderTabs = () => { + return tabs.map((tab, index) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + disabled={tab.disabled} + key={index} + > + {tab.name} + + )); + }; + + const renderOverview = () => { + return ( + + + + + + Connection title + + {datasourceDetails.name || '-'} + + + + Access control + + {datasourceDetails.allowedRoles && datasourceDetails.allowedRoles.length + ? datasourceDetails.allowedRoles + : '-'} + + + + + + + + Connection description + + {datasourceDetails.name || '-'} + + + + Connection status + + {datasourceDetails.cluster || '-'} + + + + + + + + ); + }; + + return ( + + + + + + + +

{dataSource}

+
+
+
+
+
+ + {renderOverview()} + + + + + } + title={'Query data'} + description="Query your data in Data Explorer or Observability Logs." + onClick={() => {}} + /> + + + } + title={'Accelerate performance'} + description="Accelerate performance through OpenSearch indexing." + onClick={() => {}} + /> + + + + {renderTabs()} + + +
+
+ ); +} diff --git a/public/components/data_connections/components/datasources_header.tsx b/public/components/data_connections/components/datasources_header.tsx new file mode 100644 index 0000000000..8c0ceb7242 --- /dev/null +++ b/public/components/data_connections/components/datasources_header.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiLink, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import _ from 'lodash'; +import React from 'react'; +import { OPENSEARCH_DOCUMENTATION_URL } from '../../../../common/constants/data_connections'; + +export function DataConnectionsHeader() { + return ( +
+ + + +

Data connections

+
+
+
+ + + Connect and manage compatible OpenSearch Dashboard data sources and compute.{' '} + + Learn more + + + +
+ ); +} diff --git a/public/components/data_connections/components/manage_datasource_description.tsx b/public/components/data_connections/components/manage_datasource_description.tsx new file mode 100644 index 0000000000..f05fb01e25 --- /dev/null +++ b/public/components/data_connections/components/manage_datasource_description.tsx @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiSpacer, EuiText, EuiTitle, EuiHorizontalRule } from '@elastic/eui'; +import _ from 'lodash'; +import React from 'react'; + +export function DataConnectionsDescription() { + return ( +
+ +

Manage existing data connections

+
+ + + + Manage already created data source connections. + + +
+ ); +} diff --git a/public/components/data_connections/components/manage_datasource_table.tsx b/public/components/data_connections/components/manage_datasource_table.tsx new file mode 100644 index 0000000000..2fd3e01b3b --- /dev/null +++ b/public/components/data_connections/components/manage_datasource_table.tsx @@ -0,0 +1,159 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiInMemoryTable, + EuiLink, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiTableFieldDataColumnType, + EuiText, +} from '@elastic/eui'; +import _ from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { DataConnectionsHeader } from './datasources_header'; +import { HomeProps } from '../home'; +import { DataConnectionsDescription } from './manage_datasource_description'; +import { DATASOURCES_BASE } from '../../../../common/constants/shared'; + +interface DataConnection { + connectionType: 'OPENSEARCH' | 'SPARK'; + name: string; +} + +export function ManageDatasourcesTable(props: HomeProps) { + const { http, chrome, pplService } = props; + + const [data, setData] = useState([]); + + useEffect(() => { + chrome.setBreadcrumbs([ + { + text: 'Datasources', + href: '#/', + }, + ]); + handleDataRequest(); + }, []); + + async function handleDataRequest() { + http.get(`${DATASOURCES_BASE}`).then((datasources) => + setData( + datasources.map((x: any) => { + return { name: x.name, connectionType: x.connector }; + }) + ) + ); + } + + const icon = (record: DataConnection) => { + switch (record.connectionType) { + case 'OPENSEARCH': + return ; + default: + return <>; + } + }; + + const tableColumns = [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + render: (value, record: DataConnection) => ( + + {icon(record)} + + + {_.truncate(record.name, { length: 100 })} + + + + ), + }, + { + field: 'connectionStatus', + name: 'Connection Status', + sortable: true, + truncateText: true, + render: (value, record) => ( + + {_.truncate(record.creationDate, { length: 100 })} + + ), + }, + { + field: 'actions', + name: 'Actions', + sortable: true, + truncateText: true, + render: (value, record) => ( + { + /* Delete Datasource*/ + }} + /> + ), + }, + ] as Array>; + + const search = { + box: { + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'templateName', + name: 'Type', + multiSelect: false, + options: [].map((i) => ({ + value: i, + name: i, + view: i, + })), + }, + ], + }; + + const entries = data.map((dataconnection: DataConnection) => { + const name = dataconnection.name; + const connectionType = dataconnection.connectionType; + return { connectionType, name, data: { name, connectionType } }; + }); + + return ( + + + + + + + + + + ); +} diff --git a/public/components/data_connections/home.tsx b/public/components/data_connections/home.tsx new file mode 100644 index 0000000000..88ad367c53 --- /dev/null +++ b/public/components/data_connections/home.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { ChromeBreadcrumb, ChromeStart, HttpStart } from '../../../../../src/core/public'; +import { DataSource } from './components/datasource'; +import { ManageDatasourcesTable } from './components/manage_datasource_table'; + +export interface HomeProps extends RouteComponentProps { + pplService: any; + parentBreadcrumb: ChromeBreadcrumb; + http: HttpStart; + chrome: ChromeStart; +} + +export const Home = (props: HomeProps) => { + const { http, chrome, pplService } = props; + + const commonProps = { + http, + chrome, + pplService, + }; + + return ( +
+ + + ( + + )} + /> + + } + /> + + +
+ ); +}; diff --git a/public/components/integrations/components/available_integration_overview_page.tsx b/public/components/integrations/components/available_integration_overview_page.tsx index e858daa549..360bf0db49 100644 --- a/public/components/integrations/components/available_integration_overview_page.tsx +++ b/public/components/integrations/components/available_integration_overview_page.tsx @@ -117,8 +117,7 @@ export function AvailableIntegrationOverviewPage(props: AvailableIntegrationOver http.get(`${INTEGRATIONS_BASE}/repository`).then((exists) => { setData(exists.data); - let newItems = exists.data.hits - .flatMap((hit: { labels?: string[] }) => hit.labels ?? []); + let newItems = exists.data.hits.flatMap((hit: { labels?: string[] }) => hit.labels ?? []); newItems = [...new Set(newItems)].sort().map((newItem) => { return { name: newItem, diff --git a/public/plugin.ts b/public/plugin.ts index 021a9f56ad..6d553f63cf 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -39,6 +39,9 @@ import { observabilityIntegrationsTitle, observabilityIntegrationsPluginOrder, observabilityPluginOrder, + observabilityDatasourcesID, + observabilityDatasourcesTitle, + observabilityDatasourcesPluginOrder, } from '../common/constants/shared'; import { QueryManager } from '../common/query_manager'; import { VISUALIZATION_SAVED_OBJECT } from '../common/types/observability_saved_object_attributes'; @@ -197,6 +200,23 @@ export class ObservabilityPlugin mount: appMountWithStartPage('integrations'), }); + core.application.register({ + id: observabilityDatasourcesID, + title: observabilityDatasourcesTitle, + category: DEFAULT_APP_CATEGORIES.management, + order: observabilityDatasourcesPluginOrder, + mount: appMountWithStartPage('datasources'), + }); + + setupDeps.managementOverview?.register({ + id: observabilityDatasourcesID, + title: observabilityDatasourcesTitle, + order: 9070, + description: i18n.translate('observability.datasourcesDescription', { + defaultMessage: 'Manage compatible data sources and compute with OpenSearch Dashboards.', + }), + }); + const embeddableFactory = new ObservabilityEmbeddableFactoryDefinition(async () => ({ getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, diff --git a/public/types.ts b/public/types.ts index 48f0ad9de6..75afb133bf 100644 --- a/public/types.ts +++ b/public/types.ts @@ -7,6 +7,7 @@ import { SavedObjectsClient } from '../../../src/core/server'; import { DashboardStart } from '../../../src/plugins/dashboard/public'; import { DataPublicPluginSetup } from '../../../src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { ManagementOverViewPluginSetup } from '../../../src/plugins/management_overview/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; @@ -23,6 +24,7 @@ export interface SetupDependencies { visualizations: VisualizationsSetup; data: DataPublicPluginSetup; uiActions: UiActionsStart; + managementOverview?: ManagementOverViewPluginSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/server/adaptors/opensearch_observability_plugin.ts b/server/adaptors/opensearch_observability_plugin.ts index fbdbac72be..10137b33c7 100644 --- a/server/adaptors/opensearch_observability_plugin.ts +++ b/server/adaptors/opensearch_observability_plugin.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { OPENSEARCH_PANELS_API } from '../../common/constants/shared'; +import { OPENSEARCH_DATASOURCES_API, OPENSEARCH_PANELS_API } from '../../common/constants/shared'; export function OpenSearchObservabilityPlugin(Client: any, config: any, components: any) { const clientAction = components.clientAction.factory; diff --git a/server/adaptors/ppl_plugin.ts b/server/adaptors/ppl_plugin.ts index 304d196e3f..e4714c9d00 100644 --- a/server/adaptors/ppl_plugin.ts +++ b/server/adaptors/ppl_plugin.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PPL_ENDPOINT, SQL_ENDPOINT } from '../../common/constants/shared'; +import { + OPENSEARCH_DATASOURCES_API, + PPL_ENDPOINT, + SQL_ENDPOINT, +} from '../../common/constants/shared'; export const PPLPlugin = function (Client, config, components) { const ca = components.clientAction.factory; @@ -37,4 +41,24 @@ export const PPLPlugin = function (Client, config, components) { needBody: true, method: 'POST', }); + + ppl.getDatasourceById = ca({ + url: { + fmt: `${OPENSEARCH_DATASOURCES_API.DATASOURCE}/<%=datasource%>`, + req: { + datasource: { + type: 'string', + required: true, + }, + }, + }, + method: 'GET', + }); + + ppl.getDatasources = ca({ + url: { + fmt: `${OPENSEARCH_DATASOURCES_API.DATASOURCE}`, + }, + method: 'GET', + }); }; diff --git a/server/routes/datasources/datasources_router.ts b/server/routes/datasources/datasources_router.ts new file mode 100644 index 0000000000..187be8da1c --- /dev/null +++ b/server/routes/datasources/datasources_router.ts @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { IRouter } from '../../../../../src/core/server'; +import { DATASOURCES_BASE } from '../../../common/constants/shared'; + +export function registerDatasourcesRoute(router: IRouter) { + router.get( + { + path: `${DATASOURCES_BASE}/{name}`, + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, request, response): Promise => { + try { + const dataSourcesresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.getDatasourceById', { + datasource: request.params.name, + }); + return response.ok({ + body: dataSourcesresponse, + }); + } catch (error: any) { + console.error('Issue in fetching datasource:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + + router.get( + { + path: `${DATASOURCES_BASE}`, + validate: false, + }, + async (context, request, response): Promise => { + try { + const dataSourcesresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.getDatasources'); + return response.ok({ + body: dataSourcesresponse, + }); + } catch (error: any) { + console.error('Issue in fetching datasources:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); +} diff --git a/server/routes/index.ts b/server/routes/index.ts index 4830bf58c4..d3d64f5509 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -21,6 +21,7 @@ import { registerEventAnalyticsRouter } from './event_analytics/event_analytics_ import { registerAppAnalyticsRouter } from './application_analytics/app_analytics_router'; import { registerMetricsRoute } from './metrics/metrics_rounter'; import { registerIntegrationsRoute } from './integrations/integrations_router'; +import { registerDatasourcesRoute } from './datasources/datasources_router'; export function setupRoutes({ router, client }: { router: IRouter; client: ILegacyClusterClient }) { PanelsRouter(router); @@ -42,4 +43,5 @@ export function setupRoutes({ router, client }: { router: IRouter; client: ILega registerMetricsRoute(router); registerIntegrationsRoute(router); + registerDatasourcesRoute(router); }