diff --git a/public/components/datasources/components/manage/access_control_tab.tsx b/public/components/datasources/components/manage/access_control_tab.tsx index 3f588b23b..d35f3876f 100644 --- a/public/components/datasources/components/manage/access_control_tab.tsx +++ b/public/components/datasources/components/manage/access_control_tab.tsx @@ -23,6 +23,7 @@ interface AccessControlTabProps { dataConnection: string; connector: string; properties: unknown; + allowedRoles: string[]; } export const AccessControlTab = (props: AccessControlTabProps) => { @@ -30,7 +31,11 @@ export const AccessControlTab = (props: AccessControlTabProps) => { const [roles, setRoles] = useState>([]); const [selectedQueryPermissionRoles, setSelectedQueryPermissionRoles] = useState< Array<{ label: string }> - >([]); + >( + props.allowedRoles.map((role) => { + return { label: role }; + }) + ); const { http } = coreRefs; useEffect(() => { @@ -51,7 +56,7 @@ export const AccessControlTab = (props: AccessControlTabProps) => { Query access - {[].length ? `Restricted` : '-'} + {selectedQueryPermissionRoles.length ? `Restricted` : '-'} diff --git a/public/components/datasources/components/manage/data_connection.tsx b/public/components/datasources/components/manage/data_connection.tsx index c8cf014ac..a0527e7bc 100644 --- a/public/components/datasources/components/manage/data_connection.tsx +++ b/public/components/datasources/components/manage/data_connection.tsx @@ -59,15 +59,15 @@ export const DataConnection = (props: any) => { ]); http! .get(`${DATACONNECTIONS_BASE}/${dataSource}`) - .then((data) => + .then((data) => { setDatasourceDetails({ allowedRoles: data.allowedRoles, name: data.name, cluster: data.properties['emr.cluster'], connector: data.connector, properties: data.properties, - }) - ) + }); + }) .catch((err) => { setHasAccess(false); }); @@ -80,9 +80,11 @@ export const DataConnection = (props: any) => { disabled: false, content: ( ), }, @@ -200,7 +202,7 @@ export const DataConnection = (props: any) => { - + diff --git a/public/components/datasources/components/new/configure_datasource.tsx b/public/components/datasources/components/new/configure_datasource.tsx index 61cbf5f85..ed44498c8 100644 --- a/public/components/datasources/components/new/configure_datasource.tsx +++ b/public/components/datasources/components/new/configure_datasource.tsx @@ -22,8 +22,12 @@ import { EuiButtonEmpty, EuiTextArea, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { ConfigureS3Datasource } from './configure_s3_datasource'; +import { coreRefs } from '../../../../../public/framework/core_refs'; +import { DATACONNECTIONS_BASE } from '../../../../../common/constants/shared'; +import { ReviewS3Datasource } from './review_s3_datasource_configuration'; +import { useToast } from '../../../../../public/components/common/toast'; interface ConfigureDatasourceProps { type: string; @@ -31,9 +35,18 @@ interface ConfigureDatasourceProps { export function Configure(props: ConfigureDatasourceProps) { const { type } = props; + const { http } = coreRefs; + const { setToast } = useToast(); const [name, setName] = useState(''); const [details, setDetails] = useState(''); + const [arn, setArn] = useState(''); + const [store, setStore] = useState(''); + const [roles, setRoles] = useState>([]); + const [selectedQueryPermissionRoles, setSelectedQueryPermissionRoles] = useState< + Array<{ label: string }> + >([]); + const [page, setPage] = useState<'configure' | 'review'>('configure'); const ConfigureDatasourceSteps = [ { title: 'Step 1', @@ -53,6 +66,16 @@ export function Configure(props: ConfigureDatasourceProps) { }, ]; + useEffect(() => { + http!.get('/api/v1/configuration/roles').then((data) => + setRoles( + Object.keys(data.data).map((key) => { + return { label: key }; + }) + ) + ); + }, []); + const ConfigureDatasource = (configurationProps: { datasourceType: string }) => { const { datasourceType } = configurationProps; switch (datasourceType) { @@ -63,6 +86,13 @@ export function Configure(props: ConfigureDatasourceProps) { currentDetails={details} setNameForRequest={setName} setDetailsForRequest={setDetails} + currentArn={arn} + setArnForRequest={setArn} + currentStore={store} + setStoreForRequest={setStore} + roles={roles} + selectedQueryPermissionRoles={selectedQueryPermissionRoles} + setSelectedQueryPermissionRoles={setSelectedQueryPermissionRoles} /> ); default: @@ -70,34 +100,112 @@ export function Configure(props: ConfigureDatasourceProps) { } }; - return ( - - - - - - - + const ReviewDatasourceConfiguration = (configurationProps: { datasourceType: string }) => { + const { datasourceType } = configurationProps; + switch (datasourceType) { + case 'S3': + return ( + + ); + default: + return <>; + } + }; + const ReviewSaveOrCancel = useCallback(() => { + return ( - {}} color="ghost" size="s" iconType="cross"> + { + window.location.hash = '#/new'; + }} + color="ghost" + size="s" + iconType="cross" + > Cancel - {}} color="ghost" size="s" iconType="arrowLeft"> + setPage('configure') : () => {}} + color="ghost" + size="s" + iconType="arrowLeft" + > Previous - {}} size="s" iconType="arrowRight" fill> - Review Configuration + createDatasource() + : () => { + setPage('review'); + } + } + size="s" + iconType="arrowRight" + fill + > + {page === 'configure' ? `Review Configuration` : `Connect to ${type}`} + ); + }, [page]); + + const createDatasource = () => { + http! + .post(`${DATACONNECTIONS_BASE}`, { + body: JSON.stringify({ + name, + allowedRoles: selectedQueryPermissionRoles.map((role) => role.label), + connector: 's3glue', + properties: { + 'glue.auth.type': 'iam_role', + 'glue.auth.role_arn': arn, + 'glue.indexstore.opensearch.uri': store, + 'glue.indexstore.opensearch.auth': false, + 'glue.indexstore.opensearch.region': 'us-west-2', + }, + }), + }) + .then(() => { + setToast(`Data source ${name} created successfully!`); + window.location.hash = '#/manage'; + }) + .catch((err) => { + setToast(`Data source ${name} created successfully!`); + window.location.hash = '#/manage'; + }); + }; + + return ( + + + + + + {page === 'configure' ? ( + + ) : ( + + )} + + + + ); } diff --git a/public/components/datasources/components/new/configure_s3_datasource.tsx b/public/components/datasources/components/new/configure_s3_datasource.tsx index 11d6b7588..155d29d40 100644 --- a/public/components/datasources/components/new/configure_s3_datasource.tsx +++ b/public/components/datasources/components/new/configure_s3_datasource.tsx @@ -12,22 +12,45 @@ import { EuiFormRow, EuiFieldText, EuiTextArea, + EuiButton, } from '@elastic/eui'; import React, { useState } from 'react'; import { OPENSEARCH_DOCUMENTATION_URL } from '../../../../../common/constants/data_connections'; +import { QueryPermissionsConfiguration } from '../manage/query_permissions'; interface ConfigureS3DatasourceProps { + roles: Array<{ label: string }>; + selectedQueryPermissionRoles: Array<{ label: string }>; + setSelectedQueryPermissionRoles: React.Dispatch>>; currentName: string; currentDetails: string; + currentArn: string; + currentStore: string; + setStoreForRequest: React.Dispatch>; setNameForRequest: React.Dispatch>; setDetailsForRequest: React.Dispatch>; + setArnForRequest: React.Dispatch>; } export const ConfigureS3Datasource = (props: ConfigureS3DatasourceProps) => { - const { setNameForRequest, setDetailsForRequest, currentName, currentDetails } = props; + const { + setNameForRequest, + setDetailsForRequest, + setArnForRequest, + setStoreForRequest, + currentStore, + currentName, + currentDetails, + currentArn, + roles, + selectedQueryPermissionRoles, + setSelectedQueryPermissionRoles, + } = props; const [name, setName] = useState(currentName); const [details, setDetails] = useState(currentDetails); + const [arn, setArn] = useState(currentArn); + const [store, setStore] = useState(currentStore); return (
@@ -86,6 +109,95 @@ export const ConfigureS3Datasource = (props: ConfigureS3DatasourceProps) => {

Glue authentication details

+ + + + <> + +

+ This parameters provides the authentication type information required for execution + engine to connect to glue. +

+
+ + +
+ + + <> + +

This should be the IAM role ARN

+
+ { + setArn(e.target.value); + }} + onBlur={(e) => { + setArnForRequest(e.target.value); + }} + /> + +
+ + + + +

Glue index store details

+
+ + + + <> + +

+ This parameters provides the OpenSearch cluster host information for glue. This + OpenSearch instance is used for writing index data back. +

+
+ { + setStore(e.target.value); + }} + onBlur={(e) => { + setStoreForRequest(e.target.value); + }} + /> + +
+ + + <> + +

Lorem ipsum.

+
+ + +
+ + + <> + +

Lorem ipsum.

+
+ + +
+ + Test connection + + + +
); diff --git a/public/components/datasources/components/new/review_s3_datasource_configuration.tsx b/public/components/datasources/components/new/review_s3_datasource_configuration.tsx new file mode 100644 index 000000000..ede85f204 --- /dev/null +++ b/public/components/datasources/components/new/review_s3_datasource_configuration.tsx @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiLink, + EuiFormRow, + EuiFieldText, + EuiTextArea, + EuiButton, + EuiFlexGroup, + EuiHorizontalRule, + EuiFlexItem, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { OPENSEARCH_DOCUMENTATION_URL } from '../../../../../common/constants/data_connections'; +import { QueryPermissionsConfiguration } from '../manage/query_permissions'; + +interface ConfigureS3DatasourceProps { + selectedQueryPermissionRoles: Array<{ label: string }>; + currentName: string; + currentDetails: string; + currentArn: string; + currentStore: string; +} + +export const ReviewS3Datasource = (props: ConfigureS3DatasourceProps) => { + const { + currentStore, + currentName, + currentDetails, + currentArn, + selectedQueryPermissionRoles, + } = props; + + return ( +
+ + +

{`Review S3 Data Source Configuration`}

+
+ + + +

Data source configuration

+
+ + + + + + + Data source name + + {currentName} + + + + Description + + {currentDetails} + + + + + + + + Glue authentication ARN + + {currentArn} + + + + Glue index store URI + + {currentStore} + + + + + + + + Query Permissions + + {selectedQueryPermissionRoles + ? `Restricted - ${JSON.stringify(selectedQueryPermissionRoles)}` + : 'Everyone'} + + + + + +
+
+ ); +}; diff --git a/server/adaptors/ppl_plugin.ts b/server/adaptors/ppl_plugin.ts index 563c43672..6007f913f 100644 --- a/server/adaptors/ppl_plugin.ts +++ b/server/adaptors/ppl_plugin.ts @@ -68,6 +68,14 @@ export const PPLPlugin = function (Client, config, components) { method: 'DELETE', }); + ppl.createDataSource = ca({ + url: { + fmt: `${OPENSEARCH_DATACONNECTIONS_API.DATACONNECTION}`, + }, + needBody: true, + method: 'POST', + }); + ppl.modifyDataConnection = ca({ url: { fmt: `${OPENSEARCH_DATACONNECTIONS_API.DATACONNECTION}`, diff --git a/server/routes/data_connections/data_connections_router.ts b/server/routes/data_connections/data_connections_router.ts index a65660ba4..2c996ddcc 100644 --- a/server/routes/data_connections/data_connections_router.ts +++ b/server/routes/data_connections/data_connections_router.ts @@ -103,6 +103,43 @@ export function registerDataConnectionsRoute(router: IRouter) { } ); + router.post( + { + path: `${DATACONNECTIONS_BASE}`, + validate: { + body: schema.object({ + name: schema.string(), + connector: schema.string(), + allowedRoles: schema.arrayOf(schema.string()), + properties: schema.any(), + }), + }, + }, + async (context, request, response): Promise => { + try { + const dataConnectionsresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.createDataSource', { + body: { + name: request.body.name, + connector: request.body.connector, + allowedRoles: request.body.allowedRoles, + properties: request.body.properties, + }, + }); + return response.ok({ + body: dataConnectionsresponse, + }); + } catch (error: any) { + console.error('Issue in creating data source:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + router.get( { path: `${DATACONNECTIONS_BASE}`,