diff --git a/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx b/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx index 91971f9b9b6f..ba4c079e3a72 100644 --- a/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx +++ b/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx @@ -11,7 +11,6 @@ import { EuiFieldText, EuiButton, EuiFieldPassword, - EuiPageContent, EuiHorizontalRule, EuiSpacer, EuiText, @@ -28,6 +27,8 @@ import { Header } from '../header'; export interface CredentialFormProps { docLinks: DocLinksStart; handleSubmit: (formValues: CreateCredentialItem) => void; + hideSubmit?: boolean; + callSubmit?: boolean; } export interface CredentialFormState { @@ -72,6 +73,16 @@ export class CredentialForm extends React.Component, + prevState: Readonly, + snapshot?: any + ) { + if (prevProps.callSubmit !== this.props.callSubmit) { + this.onClickSubmitForm(); + } + } + /* Validations */ isFormValid = () => { @@ -165,7 +176,7 @@ export class CredentialForm extends React.Component + <> {header} {/* Create Credential button*/} - - Create stored credential - + {this.props.hideSubmit ? null : ( + + Create stored credential + + )} - + ); }; diff --git a/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx b/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx index a37b93f4a322..a11a85a31c9c 100644 --- a/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx +++ b/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx @@ -11,6 +11,7 @@ import { EuiGlobalToastListToast, EuiLoadingSpinner, EuiOverlayMask, + EuiPageContent, } from '@elastic/eui'; import { DocLinksStart } from 'src/core/public'; @@ -104,10 +105,12 @@ export class CreateCredentialWizard extends React.Component< ) : ( <> - + + + { diff --git a/src/plugins/credential_management/public/index.ts b/src/plugins/credential_management/public/index.ts index 60b4e24d2f82..9a13b0432677 100644 --- a/src/plugins/credential_management/public/index.ts +++ b/src/plugins/credential_management/public/index.ts @@ -7,6 +7,8 @@ import './index.scss'; import { CredentialManagementPlugin } from './plugin'; +export { CredentialForm } from './components/common'; +export { CreateCredentialItem } from './components/types'; // This exports static code and TypeScript types, // as well as, OpenSearch Dashboards Platform `plugin()` initializer. export function plugin() { diff --git a/src/plugins/data_source_management/common/index.ts b/src/plugins/data_source_management/common/index.ts index 193e2a1875dc..e42b0c3fc514 100644 --- a/src/plugins/data_source_management/common/index.ts +++ b/src/plugins/data_source_management/common/index.ts @@ -5,5 +5,3 @@ export const PLUGIN_ID = 'dataSourceManagement'; export const PLUGIN_NAME = 'Data Sources'; -export const MODE_CREATE = 'Create Data Source'; -export const MODE_EDIT = 'Edit Data Source'; diff --git a/src/plugins/data_source_management/opensearch_dashboards.json b/src/plugins/data_source_management/opensearch_dashboards.json index 39877108d9c3..a58ae89f0d94 100644 --- a/src/plugins/data_source_management/opensearch_dashboards.json +++ b/src/plugins/data_source_management/opensearch_dashboards.json @@ -5,5 +5,5 @@ "ui": true, "requiredPlugins": ["management", "dataSource"], "optionalPlugins": [], - "requiredBundles": ["opensearchDashboardsReact"] + "requiredBundles": ["opensearchDashboardsReact", "credentialManagement"] } diff --git a/src/plugins/data_source_management/public/components/create_button/create_button.tsx b/src/plugins/data_source_management/public/components/create_button/create_button.tsx index a0da04273d73..f5fca8b27892 100644 --- a/src/plugins/data_source_management/public/components/create_button/create_button.tsx +++ b/src/plugins/data_source_management/public/components/create_button/create_button.tsx @@ -23,7 +23,7 @@ export const CreateButton = ({ history }: Props) => { > ); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx new file mode 100644 index 000000000000..ff64214ce7bd --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -0,0 +1,497 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { + EuiButton, + EuiFieldPassword, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiPageContent, + EuiSelectable, + EuiSpacer, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { + CreateDataSourceFormType, + CreateNewCredentialType, + CredentialsComboBoxItem, + CredentialSourceType, + DataSourceManagementContextValue, + ToastMessageItem, +} from '../../../../types'; +import { Header } from '../header'; +import { getExistingCredentials } from '../../../utils'; +import { context as contextType } from '../../../../../../opensearch_dashboards_react/public'; +import { + CreateEditDataSourceValidation, + defaultValidation, + performDataSourceFormValidation, +} from '../../../validation/datasource_form_validation'; + +export interface CreateDataSourceProps { + handleSubmit: (formValues: CreateDataSourceFormType) => void; + displayToastMessage: (msg: ToastMessageItem) => void; + displayLoadingMask: (show: boolean) => void; +} +export interface CreateDataSourceState { + formErrors: string[]; + formErrorsByField: CreateEditDataSourceValidation; + dataSourceTitle: string; + dataSourceDescription: string; + endpoint: string; + showCreateCredentialModal: boolean; + selectedCredentialSourceType: string; + selectedCredentials: CredentialsComboBoxItem[]; + availableCredentials: CredentialsComboBoxItem[]; + createCredential: CreateNewCredentialType; +} + +const defaultCreateCredentialsForm: CreateNewCredentialType = { + title: '', + description: '', + credentialMaterials: { + credentialMaterialsType: 'username_password', + credentialMaterialsContent: { + username: '', + password: '', + }, + }, +}; + +const credentialSourceOptions = [ + { value: CredentialSourceType.CreateCredential, inputDisplay: 'Create credential' }, + { value: CredentialSourceType.ExistingCredential, inputDisplay: 'Use stored credential' }, + { value: CredentialSourceType.NoAuth, inputDisplay: 'No authentication' }, +]; + +export class CreateDataSourceForm extends React.Component< + CreateDataSourceProps, + CreateDataSourceState +> { + static contextType = contextType; + public readonly context!: DataSourceManagementContextValue; + + constructor(props: CreateDataSourceProps, context: DataSourceManagementContextValue) { + super(props, context); + + this.state = { + formErrors: [], + formErrorsByField: { ...defaultValidation }, + dataSourceTitle: '', + dataSourceDescription: '', + endpoint: '', + showCreateCredentialModal: false, + selectedCredentialSourceType: credentialSourceOptions[1].value, + selectedCredentials: [], + availableCredentials: [], + createCredential: defaultCreateCredentialsForm, + }; + } + + componentDidMount() { + this.fetchAvailableCredentials(); + } + + fetchAvailableCredentials() { + const { savedObjects } = this.context.services; + this.props.displayLoadingMask(true); + getExistingCredentials(savedObjects.client) + .then((fetchedCredentials: CredentialsComboBoxItem[]) => { + if (fetchedCredentials?.length) { + this.setState({ availableCredentials: fetchedCredentials }); + } + }) + .catch(() => { + this.props.displayToastMessage({ + id: 'dataSourcesManagement.createDataSource.fetchExistingCredentialsFailMsg', + defaultMessage: 'Error while finding existing credentials.', + color: 'warning', + iconType: 'alert', + }); + }) + .finally(() => { + this.props.displayLoadingMask(false); + }); + } + + /* Validations */ + + isFormValid = () => { + const { formErrors, formErrorsByField } = performDataSourceFormValidation(this.state); + + this.setState({ + formErrors, + formErrorsByField, + }); + + return formErrors.length === 0; + }; + + /* Events */ + + onChangeTitle = (e: { target: { value: any } }) => { + this.setState({ dataSourceTitle: e.target.value }, () => { + if (this.state.formErrorsByField.title.length) { + this.isFormValid(); + } + }); + }; + + onChangeDescription = (e: { target: { value: any } }) => { + this.setState({ dataSourceDescription: e.target.value }, () => { + if (this.state.formErrorsByField.description.length) { + this.isFormValid(); + } + }); + }; + + onChangeEndpoint = (e: { target: { value: any } }) => { + this.setState({ endpoint: e.target.value }, () => { + if (this.state.formErrorsByField.endpoint.length) { + this.isFormValid(); + } + }); + }; + + onChangeCredentialSourceType = (value: string) => { + /* reset already selected existing credential */ + if ( + this.state.selectedCredentials.length && + value !== CredentialSourceType.ExistingCredential + ) { + this.resetExistingCredentialSelection(); + } + + this.setState({ selectedCredentialSourceType: value }, () => { + if (this.state.formErrors.length) { + this.isFormValid(); + } + }); + }; + + resetExistingCredentialSelection = () => { + this.setState({ + selectedCredentials: [], + availableCredentials: this.state.availableCredentials?.map((credential) => { + credential.checked = false; + return credential; + }), + }); + }; + + onClickCreateNewDataSource = () => { + if (this.isFormValid()) { + const formValues: CreateDataSourceFormType = { + title: this.state.dataSourceTitle, + description: this.state.dataSourceDescription, + endpoint: this.state.endpoint, + credentialType: this.state.selectedCredentialSourceType, + credentialId: '', + newCredential: + this.state.selectedCredentialSourceType === CredentialSourceType.CreateCredential + ? this.state.createCredential + : undefined, + }; + if (this.state.selectedCredentials?.length) { + formValues.credentialId = this.state.selectedCredentials[0].id; + } + + this.props.handleSubmit(formValues); + } + }; + + onSelectExistingCredentials = (options: CredentialsComboBoxItem[]) => { + const selectedCredentials: CredentialsComboBoxItem[] = []; + options.forEach((credential) => { + if (credential.checked === 'on') { + selectedCredentials.push(credential); + } + }); + this.setState({ availableCredentials: options, selectedCredentials }, () => { + if (this.state.formErrorsByField.credential.length) { + this.isFormValid(); + } + }); + }; + + /* Render methods */ + /* Render header*/ + renderHeader = () => { + const { docLinks } = this.context.services; + return
; + }; + + /* Render Section header*/ + renderSectionHeader = (i18nId: string, defaultMessage: string) => { + return ( + <> + +
+ +
+
+ + ); + }; + + /* Render create new credentials*/ + + onChangeCreateCredentialFormField = ( + e: { target: { value: string } }, + field: 'title' | 'description' | 'username' | 'password' + ) => { + const { + title, + description, + credentialMaterials, + }: CreateNewCredentialType = this.state.createCredential; + this.setState( + { + createCredential: { + title: field === 'title' ? e.target.value : title, + description: field === 'description' ? e.target.value : description, + credentialMaterials: { + credentialMaterialsType: credentialMaterials.credentialMaterialsType, + credentialMaterialsContent: { + username: + field === 'username' + ? e.target.value + : credentialMaterials.credentialMaterialsContent.username, + password: + field === 'password' + ? e.target.value + : credentialMaterials.credentialMaterialsContent.password, + }, + }, + }, + }, + () => { + if (this.state.formErrorsByField.createCredential[field].length) { + this.isFormValid(); + } + } + ); + }; + + renderCreateNewCredentialsForm = () => { + return ( + <> + + this.onChangeCreateCredentialFormField(e, 'title')} + /> + + + this.onChangeCreateCredentialFormField(e, 'description')} + /> + + + this.onChangeCreateCredentialFormField(e, 'username')} + /> + + + this.onChangeCreateCredentialFormField(e, 'password')} + /> + + + ); + }; + + renderExistingCredentialsSection = () => { + return ( + <> + + + + this.onSelectExistingCredentials(newOptions)} + > + {(list, search) => ( + + {search} + + {list} + + )} + + + + + + ); + }; + + /* Show Create Stored Credential modal */ + + closeModal = () => { + this.setState({ showCreateCredentialModal: false }); + }; + + renderContent = () => { + return ( + + {this.renderHeader()} + + + {/* Endpoint section */} + {this.renderSectionHeader( + 'dataSourceManagement.connectToDataSource.connectionDetails', + 'Connection Details' + )} + + + {/* Title */} + + + + + {/* Description */} + + + + + {/* Endpoint URL */} + + + + + {/* Authentication Section: */} + + + + {this.renderSectionHeader( + 'dataSourceManagement.connectToDataSource.authenticationHeader', + 'Authentication' + )} + + {/* Credential source */} + + + this.onChangeCredentialSourceType(value)} + /> + + + {/* Create New credentials */} + {this.state.selectedCredentialSourceType === CredentialSourceType.CreateCredential + ? this.renderCreateNewCredentialsForm() + : null} + + {/* Existing credentials */} + {this.state.selectedCredentialSourceType === CredentialSourceType.ExistingCredential + ? this.renderExistingCredentialsSection() + : null} + + + {/* Create Data Source button*/} + + Create a data source connection + + + + ); + }; + + render() { + return <>{this.renderContent()}; + } +} diff --git a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/index.ts b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/index.ts similarity index 51% rename from src/plugins/data_source_management/public/components/create_edit_data_source_wizard/index.ts rename to src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/index.ts index a253a999f1e4..fba4fc4a5171 100644 --- a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/index.ts +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { CreateEditDataSourceWizard } from './create_edit_data_source_wizard'; +export { CreateDataSourceForm } from './create_data_source_form'; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/header/header.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/header/header.tsx new file mode 100644 index 000000000000..2a9e63e0b953 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/header/header.tsx @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +import { EuiSpacer, EuiTitle, EuiText, EuiLink, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; + +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { DocLinksStart } from 'opensearch-dashboards/public'; +import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; +import { DataSourceManagementContext } from '../../../../types'; + +export const Header = ({ docLinks }: { docLinks: DocLinksStart }) => { + const changeTitle = useOpenSearchDashboards().services.chrome + .docTitle.change; + + const createDataSourceHeader = i18n.translate('dataSourcesManagement.createDataSourceHeader', { + defaultMessage: 'Create data source connection', + }); + + changeTitle(createDataSourceHeader); + + return ( + + +
+ +

{createDataSourceHeader}

+
+ + +

+ +
+ + + +

+
+
+
+
+ ); +}; diff --git a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/index.ts b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/header/index.ts similarity index 100% rename from src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/index.ts rename to src/plugins/data_source_management/public/components/create_data_source_wizard/components/header/index.ts diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx index 3bbf6ea5e1b5..3ced03192f9c 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx @@ -9,11 +9,17 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; import { FormattedMessage } from '@osd/i18n/react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { DataSourceEditPageItem, DataSourceManagementContext, ToastMessageItem } from '../../types'; +import { + CreateDataSourceFormType, + CreateNewCredentialType, + CredentialSourceType, + DataSourceManagementContext, + ToastMessageItem, +} from '../../types'; import { getCreateBreadcrumbs } from '../breadcrumbs'; -import { CreateEditDataSourceWizard } from '../create_edit_data_source_wizard'; -import { MODE_CREATE } from '../../../common'; -import { createSingleDataSource } from '../utils'; +import { CreateDataSourceForm } from './components/create_form'; +import { createNewCredential, createSingleDataSource } from '../utils'; +import { LoadingMask } from '../loading_mask'; type CreateDataSourceWizardProps = RouteComponentProps; @@ -29,6 +35,7 @@ const CreateDataSourceWizard: React.FunctionComponent([]); + const [isLoading, setIsLoading] = useState(false); /* Set breadcrumb */ useEffectOnce(() => { @@ -41,13 +48,23 @@ const CreateDataSourceWizard: React.FunctionComponent { + credentialType, + newCredential, + }: CreateDataSourceFormType) => { + setIsLoading(true); try { - // TODO: Add rendering spinner + /* Create new credential, if user selects that option*/ + if (credentialType === CredentialSourceType.CreateCredential && newCredential?.title) { + credentialId = await createCredential(newCredential); + } const references = []; - const attributes = { title, description, endpoint, noAuth: noAuthentication }; + const attributes = { + title, + description, + endpoint, + noAuth: credentialType === CredentialSourceType.NoAuth, + }; if (credentialId) { references.push({ id: credentialId, type: 'credential', name: 'credential' }); @@ -65,6 +82,26 @@ const CreateDataSourceWizard: React.FunctionComponent { + let newCredentialId = ''; + setIsLoading(true); + try { + newCredentialId = await createNewCredential(savedObjects.client, newCredential); + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.createDataSource.createNewCredentialsFailMsg', + defaultMessage: + 'The credential saved object creation failed with some errors. Please configure data_source.enabled and try it again.', + color: 'warning', + iconType: 'alert', + }); + } + setIsLoading(false); + return newCredentialId; }; const handleDisplayToastMessage = ({ id, defaultMessage, color, iconType }: ToastMessageItem) => { @@ -82,14 +119,22 @@ const CreateDataSourceWizard: React.FunctionComponent { + setIsLoading(show); + }; + /* Render the creation wizard */ const renderContent = () => { return ( - + <> + + {isLoading ? : null} + ); }; diff --git a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/create_edit_data_source_wizard.tsx b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/create_edit_data_source_wizard.tsx deleted file mode 100644 index 479940f55d73..000000000000 --- a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/create_edit_data_source_wizard.tsx +++ /dev/null @@ -1,473 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCheckbox, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiHorizontalRule, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiPageContent, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; -import { CredentialsComboBox } from './components/credentials_combox_box'; -import { - CredentialsComboBoxItem, - DataSourceEditPageItem, - DataSourceManagementContextValue, - ToastMessageItem, -} from '../../types'; -import { Header } from './components/header'; -import { getExistingCredentials, isValidUrl } from '../utils'; -import { MODE_CREATE, MODE_EDIT } from '../../../common'; -import { context as contextType } from '../../../../opensearch_dashboards_react/public'; - -export interface CreateEditDataSourceProps { - wizardMode: string; - existingDataSource?: DataSourceEditPageItem; - handleSubmit: (formValues: DataSourceEditPageItem) => void; - displayToastMessage: (msg: ToastMessageItem) => void; - onDeleteDataSource?: () => void; -} -export interface CreateEditDataSourceState { - formErrors: string[]; - formErrorsByField: CreateEditDataSourceValidation; - dataSourceTitle: string; - dataSourceDescription: string; - endpoint: string; - showCreateCredentialModal: boolean; - noAuthentication: boolean; - selectedCredentials: CredentialsComboBoxItem[]; - availableCredentials: CredentialsComboBoxItem[]; -} - -interface CreateEditDataSourceValidation { - title: string[]; - description: string[]; - endpoint: string[]; - credential: string[]; -} - -const defaultValidation: CreateEditDataSourceValidation = { - title: [], - description: [], - endpoint: [], - credential: [], -}; - -export class CreateEditDataSourceWizard extends React.Component< - CreateEditDataSourceProps, - CreateEditDataSourceState -> { - static contextType = contextType; - public readonly context!: DataSourceManagementContextValue; - - constructor(props: CreateEditDataSourceProps, context: DataSourceManagementContextValue) { - super(props, context); - - this.state = { - formErrors: [], - formErrorsByField: { ...defaultValidation }, - dataSourceTitle: '', - dataSourceDescription: '', - endpoint: '', - showCreateCredentialModal: false, - noAuthentication: false, - selectedCredentials: [], - availableCredentials: [], - }; - } - - componentDidMount() { - this.setFormValuesForEditMode(); - this.fetchAvailableCredentials(); - } - - async fetchAvailableCredentials() { - try { - const { savedObjects } = this.context.services; - const fetchedCredentials: CredentialsComboBoxItem[] = await getExistingCredentials( - savedObjects.client - ); - if (fetchedCredentials?.length) { - this.setState({ availableCredentials: fetchedCredentials }); - - if (this.props.wizardMode === MODE_EDIT && this.props.existingDataSource?.credentialId) { - const foundCredential = this.findCredentialById( - this.props.existingDataSource.credentialId, - fetchedCredentials - ); - this.setState({ - selectedCredentials: foundCredential && foundCredential.id ? [foundCredential] : [], - }); - } - } - } catch (e) { - this.props.displayToastMessage({ - id: 'dataSourcesManagement.createEditDataSource.fetchExistingCredentialsFailMsg', - defaultMessage: 'Error while finding existing credentials.', - color: 'warning', - iconType: 'alert', - }); - } - } - - findCredentialById(id: string, credentials: CredentialsComboBoxItem[]) { - return credentials?.find((rec) => rec.id === id); - } - - setFormValuesForEditMode() { - if (this.props.wizardMode === MODE_EDIT && this.props.existingDataSource) { - const { title, description, endpoint, noAuthentication } = this.props.existingDataSource; - this.setState({ - dataSourceTitle: title, - dataSourceDescription: description, - endpoint, - noAuthentication, - }); - } - } - - /* Validations */ - - isFormValid = () => { - const validationByField: CreateEditDataSourceValidation = { - title: [], - description: [], - endpoint: [], - credential: [], - }; - const formErrorMessages: string[] = []; - /* Title validation */ - if (!this.state.dataSourceTitle) { - validationByField.title.push('Title should not be empty'); - formErrorMessages.push('Title should not be empty'); - } - /* Description Validation */ - if (!this.state.dataSourceDescription) { - validationByField.description.push('Description should not be empty'); - formErrorMessages.push('Description should not be empty'); - } - /* Endpoint Validation */ - if (!isValidUrl(this.state.endpoint)) { - validationByField.endpoint.push('Endpoint is not valid'); - formErrorMessages.push('Endpoint is not valid'); - } - /* Credential Validation */ - if (!this.state.noAuthentication && !this.state.selectedCredentials?.length) { - validationByField.credential.push('Please associate a credential'); - formErrorMessages.push('Please associate a credential'); - } - - this.setState({ - formErrors: formErrorMessages, - formErrorsByField: { ...validationByField }, - }); - return formErrorMessages.length === 0; - }; - - /* Events */ - - /* Create new credentials*/ - onClickCreateNewCredential = () => { - this.setState({ showCreateCredentialModal: true }); - }; - - onChangeTitle = (e: { target: { value: any } }) => { - this.setState({ dataSourceTitle: e.target.value }, () => { - if (this.state.formErrorsByField.title.length) { - this.isFormValid(); - } - }); - }; - - onChangeDescription = (e: { target: { value: any } }) => { - this.setState({ dataSourceDescription: e.target.value }, () => { - if (this.state.formErrorsByField.description.length) { - this.isFormValid(); - } - }); - }; - - onChangeEndpoint = (e: { target: { value: any } }) => { - this.setState({ endpoint: e.target.value }, () => { - if (this.state.formErrorsByField.endpoint.length) { - this.isFormValid(); - } - }); - }; - - onChangeNoAuth = () => { - this.setState( - { - noAuthentication: !this.state.noAuthentication, - }, - () => { - /* When credential is already selected and user checks the noAuth checkbox, reset previously selected credential*/ - if (this.state.noAuthentication && this.state.selectedCredentials.length) { - this.setState({ selectedCredentials: [] }); - } - } - ); - }; - - onClickCreateNewDataSource = () => { - if (this.isFormValid()) { - const formValues: DataSourceEditPageItem = { - id: this.props.existingDataSource?.id || '', - title: this.state.dataSourceTitle, - description: this.state.dataSourceDescription, - endpoint: this.state.endpoint, - noAuthentication: this.state.noAuthentication, - credentialId: '', - }; - if (this.state.selectedCredentials?.length) { - formValues.credentialId = this.state.selectedCredentials[0].id; - } - - this.props.handleSubmit(formValues); - } - }; - - onSelectExistingCredentials = (options: CredentialsComboBoxItem[]) => { - this.setState({ selectedCredentials: options }, () => { - /* When noAuth checkbox is checked and user selects credentials, un-check the noAuth checkbox*/ - if (options.length && this.state.noAuthentication) { - this.onChangeNoAuth(); - } - if (this.state.formErrorsByField.credential.length) { - this.isFormValid(); - } - }); - }; - - onCreateStoredCredential = () => { - /* TODO */ - }; - - onClickDeleteDataSource = () => { - if (this.props.onDeleteDataSource) { - this.props.onDeleteDataSource(); - } - }; - - /* Render methods */ - /* Render header*/ - renderHeader = () => { - const { docLinks } = this.context.services; - return ( -
- ); - }; - - /* Render Section header*/ - renderAuthenticationSectionHeader = () => { - return ( - <> - - -
- -
- -

- -

-
-
- - ); - }; - - renderCredentialsSection = () => { - return ( - <> - - - - - - - - - - Create New Stored Credential - - - - - {this.renderCreateStoredCredentialModal()} - - ); - }; - - /* Show Create Stored Credential modal */ - - closeModal = () => { - this.setState({ showCreateCredentialModal: false }); - }; - - renderCreateStoredCredentialModal() { - let modal; - - if (this.state.showCreateCredentialModal) { - modal = ( - - - -

- -

-
-
- - -

- -

-
- - - Cancel - - - Create & Add - - -
- ); - } - return
{modal}
; - } - - renderContent = () => { - return ( - - {this.renderHeader()} - - - {/* Endpoint section */} - - {/* Title */} - - - - - {/* Description */} - - - - - {/* Endpoint URL */} - - - - - {/* Authentication Section: */} - - {this.renderAuthenticationSectionHeader()} - - {this.renderCredentialsSection()} - - - - - - - {/* Create Data Source button*/} - - {this.props.wizardMode === MODE_CREATE ? 'Create a data source connection' : 'Update'} - - - - ); - }; - - render() { - return <>{this.renderContent()}; - } -} 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 a85cc1ab178f..ce053f86e97b 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 @@ -6,9 +6,13 @@ import { EuiBadge, EuiBadgeGroup, + EuiButton, EuiButtonEmpty, + EuiConfirmModal, EuiFlexGroup, EuiFlexItem, + EuiGlobalToastList, + EuiGlobalToastListToast, EuiInMemoryTable, EuiPageContent, EuiSpacer, @@ -25,9 +29,10 @@ import { reactRouterNavigate, useOpenSearchDashboards, } from '../../../../opensearch_dashboards_react/public'; -import { DataSourceManagementContext, DataSourceTableItem } from '../../types'; +import { DataSourceManagementContext, DataSourceTableItem, ToastMessageItem } from '../../types'; import { CreateButton } from '../create_button'; -import { getDataSources } from '../utils'; +import { deleteMultipleDataSources, getDataSources } from '../utils'; +import { LoadingMask } from '../loading_mask'; /* Table config */ const pagination = { @@ -42,42 +47,100 @@ const sorting = { }, }; -const search = { - box: { - incremental: true, - schema: { - fields: { title: { type: 'string' } }, - }, - }, -}; - const ariaRegion = i18n.translate('dataSourcesManagement.createDataSourcesLiveRegionAriaLabel', { defaultMessage: 'Data Sources', }); const title = i18n.translate('dataSourcesManagement.dataSourcesTable.title', { defaultMessage: 'Data Sources', }); +/* Browser - Page Title */ +const pageTitle = i18n.translate('dataSourcesManagement.objects.dataSourcesTitle', { + defaultMessage: 'Data Sources', +}); + +const toastLifeTimeMs = 6000; export const DataSourceTable = ({ history }: RouteComponentProps) => { - const { setBreadcrumbs, savedObjects } = useOpenSearchDashboards< + const { chrome, setBreadcrumbs, savedObjects } = useOpenSearchDashboards< DataSourceManagementContext >().services; /* Component state variables */ const [dataSources, setDataSources] = useState([]); + const [selectedDataSources, setSelectedDataSources] = useState([]); + const [toasts, setToasts] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(false); + const [isDeleting, setIsDeleting] = React.useState(false); + const [confirmDeleteVisible, setConfirmDeleteVisible] = React.useState(false); /* useEffectOnce hook to avoid these methods called multiple times when state is updated. */ useEffectOnce(() => { /* Update breadcrumb*/ setBreadcrumbs(getListBreadcrumbs()); - /* Initialize the component state*/ - (async function () { - const fetchedDataSources: DataSourceTableItem[] = await getDataSources(savedObjects.client); - setDataSources(fetchedDataSources); - })(); + /* Browser - Page Title */ + chrome.docTitle.change(pageTitle); + + /* fetch data sources*/ + fetchDataSources(); }); + const fetchDataSources = () => { + setIsLoading(true); + getDataSources(savedObjects.client) + .then((response: DataSourceTableItem[]) => { + setDataSources(response); + }) + .catch(() => { + setDataSources([]); + handleDisplayToastMessage({ + id: 'dataSourcesManagement.dataSourceListing.fetchDataSourceFailMsg', + defaultMessage: + 'Error occurred while fetching the records for Data sources. Please try it again', + color: 'warning', + iconType: 'alert', + }); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + /* Table search config */ + const renderDeleteButton = () => { + return ( + { + setConfirmDeleteVisible(true); + }} + disabled={selectedDataSources.length === 0} + > + Delete {selectedDataSources.length || ''} connection + {selectedDataSources.length >= 2 ? 's' : ''} + + ); + }; + + const renderToolsRight = () => { + return ( + + {renderDeleteButton()} + + ); + }; + + const search = { + toolsRight: renderToolsRight(), + box: { + incremental: true, + schema: { + fields: { title: { type: 'string' } }, + }, + }, + }; + /* Table columns */ const columns = [ { @@ -109,14 +172,105 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { dataType: 'string' as const, sortable: ({ sort }: { sort: string }) => sort, }, + { + field: 'description', + name: 'Description', + truncateText: true, + mobileOptions: { + show: false, + }, + dataType: 'string' as const, + sortable: ({ sort }: { sort: string }) => sort, + }, ]; + /* render delete modal*/ + const tableRenderDeleteModal = () => { + return confirmDeleteVisible ? ( + { + setConfirmDeleteVisible(false); + }} + onConfirm={() => { + setConfirmDeleteVisible(false); + onClickDelete(); + }} + cancelButtonText="Cancel" + confirmButtonText="Delete" + defaultFocusedButton="confirm" + > +

+ This will delete data source connections(s) and all Index Patterns using this credential + will be invalid for access. +

+

To confirm deletion, click delete button.

+

Note: this action is irrevocable!

+
+ ) : null; + }; + + /* Delete selected data sources*/ + const onClickDelete = () => { + setIsDeleting(true); + + deleteMultipleDataSources(savedObjects.client, selectedDataSources) + .then(() => { + setSelectedDataSources([]); + // Fetch data sources + fetchDataSources(); + setConfirmDeleteVisible(false); + }) + .catch(() => { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.dataSourceListing.deleteDataSourceFailMsg', + defaultMessage: + 'Error occurred while deleting few/all selected records for Data sources. Please try it again', + color: 'warning', + iconType: 'alert', + }); + }) + .finally(() => { + setIsDeleting(false); + }); + }; + + /* Table selection handlers */ + const onSelectionChange = (selected: DataSourceTableItem[]) => { + setSelectedDataSources(selected); + }; + + const selection = { + onSelectionChange, + }; + + /* Toast Handlers */ + const removeToast = (id: string) => { + setToasts(toasts.filter((toast) => toast.id !== id)); + }; + + const handleDisplayToastMessage = ({ id, defaultMessage, color, iconType }: ToastMessageItem) => { + if (id && defaultMessage && color && iconType) { + const failureMsg = ; + setToasts([ + ...toasts, + { + title: failureMsg, + id: failureMsg.props.id, + color, + iconType, + }, + ]); + } + }; + + /* Render Ui elements*/ /* Create Data Source button */ const createButton = ; - /* UI Elements */ - return ( - + /* Render header*/ + const renderHeader = () => { + return ( @@ -134,18 +288,61 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { {createButton} - - { + return ( + <> + + {/* Header */} + {renderHeader()} + + + + {/* Delete confirmation modal*/} + {tableRenderDeleteModal()} + + {/* Data sources table*/} + + + {isDeleting ? : null} + + ); + }; + + const renderContent = () => { + return ( + <> + {renderTableContent()} + {} + + ); + }; + + return ( + <> + {renderContent()} + { + removeToast(id); + }} + toastLifeTimeMs={toastLifeTimeMs} /> - + ); }; diff --git a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/credentials_combo_box.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/credentials_combox_box/credentials_combo_box.tsx similarity index 100% rename from src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/credentials_combo_box.tsx rename to src/plugins/data_source_management/public/components/edit_data_source/components/credentials_combox_box/credentials_combo_box.tsx diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx new file mode 100644 index 000000000000..7826d6181452 --- /dev/null +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -0,0 +1,665 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiBottomBar, + EuiButton, + EuiButtonEmpty, + EuiCard, + EuiDescribedFormGroup, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiPanel, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { + CreateNewCredentialType, + CredentialsComboBoxItem, + CredentialSourceType, + DataSourceManagementContextValue, + EditDataSourceFormType, + ToastMessageItem, +} from '../../../../types'; +import { Header } from '../header'; +import { createNewCredential, getExistingCredentials } from '../../../utils'; +import { context as contextType } from '../../../../../../opensearch_dashboards_react/public'; +import { + CreateEditDataSourceValidation, + defaultValidation, + performDataSourceFormValidation, +} from '../../../validation/datasource_form_validation'; +import { CredentialsComboBox } from '../credentials_combox_box/credentials_combo_box'; +import { + CredentialForm, + CreateCredentialItem, +} from '../../../../../../credential_management/public'; + +export interface EditDataSourceProps { + existingDataSource: EditDataSourceFormType; + handleSubmit: (formValues: EditDataSourceFormType) => void; + displayToastMessage: (msg: ToastMessageItem) => void; + displayLoadingMask: (show: boolean) => void; + onDeleteDataSource?: () => void; +} +export interface EditDataSourceState { + formErrors: string[]; + formErrorsByField: CreateEditDataSourceValidation; + dataSourceTitle: string; + dataSourceDescription: string; + endpoint: string; + selectedCredentialSourceType: string; + selectedCredentials: CredentialsComboBoxItem[]; + availableCredentials: CredentialsComboBoxItem[]; + showCreateCredentialModal: boolean; + showUpdateOptions: boolean; + onClickNewCredential: boolean; +} + +const noAuthOption: CredentialsComboBoxItem = { + title: 'No Authentication', + description: 'No Authentication', + checked: null, + id: CredentialSourceType.NoAuth, + credentialtype: CredentialSourceType.NoAuth, + label: 'No Authentication', +}; + +export class EditDataSourceForm extends React.Component { + static contextType = contextType; + public readonly context!: DataSourceManagementContextValue; + + constructor(props: EditDataSourceProps, context: DataSourceManagementContextValue) { + super(props, context); + + this.state = { + formErrors: [], + formErrorsByField: { ...defaultValidation }, + dataSourceTitle: '', + dataSourceDescription: '', + endpoint: '', + selectedCredentialSourceType: CredentialSourceType.NoAuth, + selectedCredentials: [], + availableCredentials: [], + showCreateCredentialModal: false, + showUpdateOptions: false, + onClickNewCredential: false, + }; + } + + componentDidMount() { + this.setFormValuesForEditMode(); + this.fetchAvailableCredentials(this.props.existingDataSource.credentialId); + } + + fetchAvailableCredentials(selectedCredentialId: string) { + const { savedObjects } = this.context.services; + this.props.displayLoadingMask(true); + getExistingCredentials(savedObjects.client) + .then((fetchedCredentials: CredentialsComboBoxItem[]) => { + if (fetchedCredentials?.length) { + this.setState({ availableCredentials: [{ ...noAuthOption }, ...fetchedCredentials] }); + this.setSelectedCredential(fetchedCredentials, selectedCredentialId); + } + }) + .catch(() => { + this.props.displayToastMessage({ + id: 'dataSourcesManagement.createEditDataSource.fetchExistingCredentialsFailMsg', + defaultMessage: 'Error while finding existing credentials.', + color: 'warning', + iconType: 'alert', + }); + }) + .finally(() => { + this.props.displayLoadingMask(false); + }); + } + + findCredentialById(id: string, credentials: CredentialsComboBoxItem[]) { + return credentials?.find((rec) => rec.id === id); + } + + resetFormValues = () => { + this.setFormValuesForEditMode(); + if (this.state.availableCredentials.length) { + this.setSelectedCredential( + this.state.availableCredentials, + this.props.existingDataSource.credentialId + ); + } + this.setState({ showUpdateOptions: false }); + }; + + setFormValuesForEditMode() { + if (this.props.existingDataSource) { + const { title, description, endpoint } = this.props.existingDataSource; + this.setState({ + dataSourceTitle: title, + dataSourceDescription: description, + endpoint, + }); + } + } + + setSelectedCredential = (fetchedCredentials: CredentialsComboBoxItem[], selectedId: string) => { + if (selectedId !== CredentialSourceType.NoAuth) { + const foundCredential = this.findCredentialById(selectedId, fetchedCredentials); + this.setState({ + selectedCredentials: foundCredential && foundCredential.id ? [foundCredential] : [], + selectedCredentialSourceType: + foundCredential && foundCredential.id ? CredentialSourceType.ExistingCredential : '', + }); + } else { + this.setState({ + selectedCredentials: [{ ...this.state.availableCredentials[0] }], + selectedCredentialSourceType: CredentialSourceType.NoAuth, + }); + } + this.onChangeFormValues(); + }; + + /* Validations */ + + isFormValid = () => { + const { formErrors, formErrorsByField } = performDataSourceFormValidation(this.state); + + this.setState({ + formErrors, + formErrorsByField, + }); + + return formErrors.length === 0; + }; + + /* Events */ + + onChangeTitle = (e: { target: { value: any } }) => { + this.setState({ dataSourceTitle: e.target.value }, () => { + if (this.state.formErrorsByField.title.length) { + this.isFormValid(); + } + }); + }; + + onChangeDescription = (e: { target: { value: any } }) => { + this.setState({ dataSourceDescription: e.target.value }, () => { + if (this.state.formErrorsByField.description.length) { + this.isFormValid(); + } + }); + }; + + onChangeEndpoint = (e: { target: { value: any } }) => { + this.setState({ endpoint: e.target.value }, () => { + if (this.state.formErrorsByField.endpoint.length) { + this.isFormValid(); + } + }); + }; + + onClickUpdateDataSource = () => { + if (this.isFormValid()) { + const formValues: EditDataSourceFormType = { + id: this.props.existingDataSource.id, + title: this.state.dataSourceTitle, + description: this.state.dataSourceDescription, + endpoint: this.state.endpoint, + credentialType: this.state.selectedCredentialSourceType, + credentialId: this.state.selectedCredentials?.length + ? this.state.selectedCredentials[0].id + : '', + }; + + this.props.handleSubmit(formValues); + } + }; + + onSelectExistingCredentials = (options: CredentialsComboBoxItem[]) => { + this.setState( + { + selectedCredentials: options, + selectedCredentialSourceType: options?.length ? options[0].credentialtype : '', + }, + () => { + this.onChangeFormValues(); + if (this.state.formErrorsByField.credential.length) { + this.isFormValid(); + } + } + ); + }; + + onClickDeleteDataSource = () => { + if (this.props.onDeleteDataSource) { + this.props.onDeleteDataSource(); + } + }; + + onChangeFormValues = () => { + setTimeout(() => { + this.didFormValuesChange(); + }, 0); + }; + + /* Create new credentials*/ + onClickCreateNewCredential = () => { + this.setState({ showCreateCredentialModal: true }); + }; + + handleCreateNewCredential = ({ + title, + description, + username, + password, + }: CreateCredentialItem) => { + const { savedObjects } = this.context.services; + this.setState({ showCreateCredentialModal: false }); + const attributes: CreateNewCredentialType = { + title, + description: description || '', + credentialMaterials: { + credentialMaterialsType: 'username_password', + credentialMaterialsContent: { + username, + password, + }, + }, + }; + createNewCredential(savedObjects.client, attributes) + .then((response) => { + this.fetchAvailableCredentials(response); + }) + .catch(() => { + this.props.displayToastMessage({ + id: 'dataSourcesManagement.editDataSource.createCredentialsFailMsg', + defaultMessage: 'Error while creating new credential.', + color: 'warning', + iconType: 'alert', + }); + }); + }; + + /* Create new credentials*/ + onCreateNewCredential = () => { + const value = !this.state.onClickNewCredential; + this.setState({ onClickNewCredential: value }); + }; + + /* Render methods */ + + /* Render Modal for new credential */ + closeModal = () => { + this.setState({ showCreateCredentialModal: false }); + }; + renderCreateStoredCredentialModal() { + let modal; + const { docLinks } = this.context.services; + + if (this.state.showCreateCredentialModal) { + modal = ( + + + + + + + + + Cancel + + + Create & Add + + + + ); + } + return
{modal}
; + } + /* Render header*/ + renderHeader = () => { + return ( +
+ ); + }; + + /* Render Connection Details Panel */ + renderConnectionDetailsSection = () => { + return ( + + Connection Details + + + + Object Details} + description={ +

+ This connection information is used for reference in tables and when adding to a data + source connection +

+ } + > + {/* Title */} + + + + {/* Description */} + + + +
+
+ ); + }; + + /* Render Connection Details Panel */ + renderEndpointSection = () => { + return ( + + Endpoint + + + + Endpoint URL} + description={ +

+ This connection information is used for reference in tables and when adding to a data + source connection +

+ } + > + {/* Endpoint */} + + + +
+
+ ); + }; + + /* Render Connection Details Panel */ + renderAuthenticationSection = () => { + return ( + + Authentication + + + + Associated Credential} + description={ +

+ Remove or replace the associated credential. To edit an existing credential, visit +

+ } + > + {/* Existing Credential*/} + {this.renderCredentialsSection()} + + + + {/* Credential Card */} + {this.renderCredentialCard()} +
+
+ ); + }; + + /* Render Credentials Existing & new */ + renderCredentialsSection = () => { + return ( + <> + + + + + + + + + + Create + + + + + {this.renderCreateStoredCredentialModal()} + + ); + }; + + /* Render Credential Card */ + + renderCredentialCardTitle = () => { + return !this.state.selectedCredentials?.length + ? 'No Credential Associated' + : this.state.selectedCredentials[0].title; + }; + + renderCredentialCardDescriptionByType = () => { + if (!this.state.selectedCredentials?.length) { + return

A credential has not been associated or the credential object has been removed

; + } else { + const { title } = this.state.selectedCredentials[0]; + if (title === 'No Authentication') { + return

No authentication is required

; + } else { + return ( +
+ +
Credential Description
+

{this.state.selectedCredentials[0].description}

+
Authentication Method
+

+ {this.state.selectedCredentials[0].credentialtype.includes('username_password') + ? 'Username & Password' + : 'Other'} +

+
Credential Source
+

Stored Credentials

+
+
+ ); + } + } + }; + + renderCredentialCard = () => { + return ( + + {this.renderCredentialCardDescriptionByType()} + + ); + }; + + didFormValuesChange = () => { + const formValues: EditDataSourceFormType = { + id: this.props.existingDataSource.id, + title: this.state.dataSourceTitle, + description: this.state.dataSourceDescription, + endpoint: this.state.endpoint, + credentialType: this.state.selectedCredentialSourceType, + credentialId: this.state.selectedCredentials?.length + ? this.state.selectedCredentials[0].id + : '', + }; + + const { + title, + description, + endpoint, + credentialType, + credentialId, + } = this.props.existingDataSource; + + if ( + formValues.title !== title || + formValues.description !== description || + formValues.endpoint !== endpoint || + formValues.credentialId !== credentialId || + formValues.credentialType !== credentialType + ) { + this.setState({ showUpdateOptions: true }); + } else { + this.setState({ showUpdateOptions: false }); + } + }; + + renderBottomBar = () => { + let bottomBar = null; + + if (this.state.showUpdateOptions) { + bottomBar = ( + + + + + + this.resetFormValues()} + aria-describedby="aria-describedby.countOfUnsavedSettings" + data-test-subj="advancedSetting-cancelButton" + > + {i18n.translate('advancedSettings.form.cancelButtonLabel', { + defaultMessage: 'Cancel changes', + })} + + + + + + {i18n.translate('advancedSettings.form.saveButtonLabel', { + defaultMessage: 'Save changes', + })} + + + + + + ); + } + return bottomBar; + }; + + renderContent = () => { + return ( + <> + {this.renderHeader()} + this.onChangeFormValues()} + data-test-subj="data-source-creation" + isInvalid={!!this.state.formErrors.length} + error={this.state.formErrors} + > + {this.renderConnectionDetailsSection()} + + + + {this.renderEndpointSection()} + + + + {this.renderAuthenticationSection()} + + + {this.renderBottomBar()} + + + + + ); + }; + + render() { + return <>{this.renderContent()}; + } +} diff --git a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/header.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx similarity index 64% rename from src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/header.tsx rename to src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx index 40aa780564f1..922bac06697f 100644 --- a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/header.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx @@ -9,9 +9,6 @@ import { EuiBetaBadge, EuiSpacer, EuiTitle, - EuiText, - EuiCode, - EuiLink, EuiFlexItem, EuiFlexGroup, EuiToolTip, @@ -20,25 +17,19 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import { FormattedMessage } from '@osd/i18n/react'; -import { DocLinksStart } from 'opensearch-dashboards/public'; import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; import { DataSourceManagementContext } from '../../../../types'; export const Header = ({ - prompt, showDeleteIcon, onClickDeleteIcon, - dataSourceName, isBeta = false, - docLinks, + dataSourceName, }: { - prompt?: React.ReactNode; - dataSourceName: string; showDeleteIcon: boolean; onClickDeleteIcon: () => void; isBeta?: boolean; - docLinks: DocLinksStart; + dataSourceName: string; }) => { /* State Variables */ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -46,12 +37,7 @@ export const Header = ({ const changeTitle = useOpenSearchDashboards().services.chrome .docTitle.change; - const createDataSourceHeader = i18n.translate('dataSourcesManagement.createDataSourceHeader', { - defaultMessage: ` ${dataSourceName}`, - values: { dataSourceName }, - }); - - changeTitle(createDataSourceHeader); + changeTitle(dataSourceName); const renderDeleteButton = () => { return ( @@ -63,6 +49,8 @@ export const Header = ({ setIsDeleteModalVisible(true); }} iconType="trash" + iconSize="m" + size="m" aria-label="Delete this data source" /> @@ -99,7 +87,7 @@ export const Header = ({

- {createDataSourceHeader} + {dataSourceName} {isBeta ? ( <> - -

- multiple, - single: filebeat-4-3-22, - star: filebeat-*, - }} - /> -
- - - -

-
- {prompt ? ( - <> - - {prompt} - - ) : null}

{showDeleteIcon ? renderDeleteButton() : null} diff --git a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/index.ts b/src/plugins/data_source_management/public/components/edit_data_source/components/header/index.ts similarity index 57% rename from src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/index.ts rename to src/plugins/data_source_management/public/components/edit_data_source/components/header/index.ts index 8ba82a533804..3c25d4c42f03 100644 --- a/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/index.ts +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { CredentialsComboBox } from './credentials_combo_box'; +export { Header } from './header'; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx index bf9d9f1e9b8e..6fe8b15fe857 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx @@ -9,19 +9,24 @@ import { useEffectOnce } from 'react-use'; import { EuiGlobalToastList, EuiGlobalToastListToast } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { DataSourceEditPageItem, DataSourceManagementContext, ToastMessageItem } from '../../types'; -import { CreateEditDataSourceWizard } from '../create_edit_data_source_wizard'; -import { MODE_EDIT } from '../../../common'; +import { + CredentialSourceType, + DataSourceManagementContext, + EditDataSourceFormType, + ToastMessageItem, +} from '../../types'; import { deleteDataSourceById, getDataSourceById, updateDataSourceById } from '../utils'; import { getEditBreadcrumbs } from '../breadcrumbs'; +import { EditDataSourceForm } from './components/edit_form/edit_data_source_form'; +import { LoadingMask } from '../loading_mask'; -const defaultDataSource: DataSourceEditPageItem = { +const defaultDataSource: EditDataSourceFormType = { id: '', title: '', description: '', endpoint: '', credentialId: '', - noAuthentication: false, + credentialType: CredentialSourceType.NoAuth, }; const EditDataSource: React.FunctionComponent> = ( @@ -33,7 +38,8 @@ const EditDataSource: React.FunctionComponent().services; /* State Variables */ - const [dataSource, setDataSource] = useState(defaultDataSource); + const [dataSource, setDataSource] = useState(defaultDataSource); + const [isLoading, setIsLoading] = useState(false); const toastLifeTimeMs: number = 6000; @@ -43,6 +49,7 @@ const EditDataSource: React.FunctionComponent { (async function () { + setIsLoading(true); try { const fetchDataSourceById = await getDataSourceById( props.match.params.id, @@ -63,6 +70,7 @@ const EditDataSource: React.FunctionComponent { + credentialType, + }: EditDataSourceFormType) => { + setIsLoading(true); try { - // TODO: Add rendering spanner https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2050 - const references = []; - const attributes = { title, description, endpoint, noAuth: noAuthentication }; - - if (credentialId) { + const attributes = { + title, + description, + endpoint, + noAuth: credentialType === CredentialSourceType.NoAuth, + }; + + if (!attributes.noAuth && credentialId) { references.push({ id: credentialId, type: 'credential', name: 'credential' }); } const options = { references }; @@ -97,6 +109,7 @@ const EditDataSource: React.FunctionComponent { @@ -116,6 +129,7 @@ const EditDataSource: React.FunctionComponent { + setIsLoading(true); try { await deleteDataSourceById(props.match.params.id, savedObjects.client); props.history.push(''); @@ -127,18 +141,30 @@ const EditDataSource: React.FunctionComponent { + setIsLoading(show); }; /* Render the edit wizard */ const renderContent = () => { + if (!isLoading && (!dataSource || !dataSource.id)) { + return

Data Source not found!

; + } return ( - + <> + + {isLoading ? : null} + ); }; diff --git a/src/plugins/data_source_management/public/components/loading_mask/index.ts b/src/plugins/data_source_management/public/components/loading_mask/index.ts new file mode 100644 index 000000000000..c1251b1cac9b --- /dev/null +++ b/src/plugins/data_source_management/public/components/loading_mask/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { LoadingMask } from './loading_mask'; diff --git a/src/plugins/data_source_management/public/components/loading_mask/loading_mask.tsx b/src/plugins/data_source_management/public/components/loading_mask/loading_mask.tsx new file mode 100644 index 000000000000..4868b6d9911e --- /dev/null +++ b/src/plugins/data_source_management/public/components/loading_mask/loading_mask.tsx @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiLoadingSpinner, EuiOverlayMask } from '@elastic/eui'; + +export const LoadingMask = () => { + return ( + + + + ); +}; diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index d7f9ed724edc..e500139e3ea3 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -4,13 +4,14 @@ */ import { SavedObjectsClientContract } from 'src/core/public'; +import { CreateNewCredentialType, CredentialSourceType, DataSourceTableItem } from '../types'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return ( savedObjectsClient .find({ type: 'data-source', - fields: ['id', 'type', 'title'], + fields: ['id', 'description', 'title'], perPage: 10000, }) .then((response) => @@ -18,10 +19,12 @@ export async function getDataSources(savedObjectsClient: SavedObjectsClientContr .map((source) => { const id = source.id; const title = source.get('title'); + const description = source.get('description'); return { id, title, + description, sort: `${title}`, }; }) @@ -58,8 +61,10 @@ export async function getDataSourceById( title: attributes.title, endpoint: attributes.endpoint, description: attributes.description || '', - credentialId, - noAuthentication: !!attributes.noAuth, + credentialId: attributes.noAuth ? CredentialSourceType.NoAuth : credentialId, + credentialType: credentialId + ? CredentialSourceType.ExistingCredential + : CredentialSourceType.NoAuth, }; }) || null ); @@ -89,19 +94,45 @@ export async function deleteDataSourceById( return savedObjectsClient.delete('data-source', id); } +export async function deleteMultipleDataSources( + savedObjectsClient: SavedObjectsClientContract, + selectedDataSources: DataSourceTableItem[] +) { + await Promise.all( + selectedDataSources.map(async (selectedDataSource) => { + await deleteDataSourceById(selectedDataSource.id, savedObjectsClient); + }) + ); +} + +export async function createNewCredential( + savedObjectsClient: SavedObjectsClientContract, + newCredential: CreateNewCredentialType +) { + return ( + savedObjectsClient.create('credential', newCredential).then((response) => response.id || '') || + null + ); +} + export async function getExistingCredentials(savedObjectsClient: SavedObjectsClientContract) { const type: string = 'credential'; - const fields: string[] = ['id', 'title']; + const fields: string[] = ['id', 'description', 'title', 'credentialMaterials']; const perPage: number = 10000; return savedObjectsClient.find({ type, fields, perPage }).then( (response) => response.savedObjects.map((source) => { const id = source.id; const title = source.get('title'); + const description = source.get('description'); + const credentialtype = source.get('credentialMaterials')?.credentialMaterialsType; return { id, title, + description, + credentialtype, label: `${title}`, + checked: null, }; }) || [] ); diff --git a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts new file mode 100644 index 000000000000..a3b808f15d9d --- /dev/null +++ b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts @@ -0,0 +1,113 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isValidUrl } from '../utils'; +import { CredentialSourceType } from '../../types'; +import { CreateEditDataSourceState } from '../create_edit_data_source_wizard/create_edit_data_source_wizard'; + +export interface CreateEditDataSourceValidation { + title: string[]; + description: string[]; + endpoint: string[]; + credential: string[]; + createCredential: { + title: string[]; + description: string[]; + username: string[]; + password: string[]; + }; +} + +export const defaultValidation: CreateEditDataSourceValidation = { + title: [], + description: [], + endpoint: [], + credential: [], + createCredential: { + title: [], + description: [], + username: [], + password: [], + }, +}; + +export const performDataSourceFormValidation = (formValues: CreateEditDataSourceState) => { + const validationByField: CreateEditDataSourceValidation = { + title: [], + description: [], + endpoint: [], + credential: [], + createCredential: { + title: [], + description: [], + username: [], + password: [], + }, + }; + const formErrorMessages: string[] = []; + /* Title validation */ + if (!formValues.dataSourceTitle) { + validationByField.title.push('Title should not be empty'); + formErrorMessages.push('Title should not be empty'); + } + /* Description Validation */ + if (!formValues.dataSourceDescription) { + validationByField.description.push('Description should not be empty'); + formErrorMessages.push('Description should not be empty'); + } + /* Endpoint Validation */ + if (!isValidUrl(formValues.endpoint)) { + validationByField.endpoint.push('Endpoint is not valid'); + formErrorMessages.push('Endpoint is not valid'); + } + + /* Credential Validation */ + /* Existing Credential */ + + if (!formValues.selectedCredentialSourceType) { + validationByField.credential.push('Please associate a credential'); + formErrorMessages.push('Please associate a credential'); + } + + if ( + formValues.selectedCredentialSourceType === CredentialSourceType.ExistingCredential && + !formValues.selectedCredentials?.length + ) { + validationByField.credential.push('Please associate a credential'); + formErrorMessages.push('Please associate a credential'); + } + + /* Create new credentials */ + if (formValues.selectedCredentialSourceType === CredentialSourceType.CreateCredential) { + /* title */ + if (!formValues.createCredential.title) { + validationByField.createCredential.title.push('Title should not be empty'); + formErrorMessages.push('New credential - Title should not be empty'); + } + + /* description */ + if (!formValues.createCredential.description) { + validationByField.createCredential.description.push('Description should not be empty'); + formErrorMessages.push('New credential - Description should not be empty'); + } + + /* Username */ + if (!formValues.createCredential.credentialMaterials.credentialMaterialsContent.username) { + validationByField.createCredential.username.push('Username should not be empty'); + formErrorMessages.push('New credential - Username should not be empty'); + } + + /* password */ + if (!formValues.createCredential.credentialMaterials.credentialMaterialsContent.password) { + validationByField.createCredential.password.push('Password should not be empty'); + formErrorMessages.push('New credential - Password should not be empty'); + } + } + + return { + formErrors: formErrorMessages, + formErrorsByField: { ...validationByField }, + }; +}; diff --git a/src/plugins/data_source_management/public/management_app/mount_management_section.tsx b/src/plugins/data_source_management/public/management_app/mount_management_section.tsx index e82dc171a76f..9fe1f2406382 100644 --- a/src/plugins/data_source_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_source_management/public/management_app/mount_management_section.tsx @@ -6,7 +6,6 @@ import { StartServicesAccessor } from 'src/core/public'; import { I18nProvider } from '@osd/i18n/react'; -import { i18n } from '@osd/i18n'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; @@ -43,13 +42,6 @@ export async function mountManagementSection( setBreadcrumbs: params.setBreadcrumbs, }; - /* Browser - Page Title */ - const title = i18n.translate('dataSourcesManagement.objects.dataSourcesTitle', { - defaultMessage: 'Data Sources', - }); - - chrome.docTitle.change(title); - ReactDOM.render( diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index b66d5205330a..4f93e654063a 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -34,22 +34,35 @@ export interface DataSourceManagementContext { export interface DataSourceTableItem { id: string; title: string; + description: string; sort: string; } export interface CredentialsComboBoxItem { + checked: boolean | 'on' | 'off' | null; id: string; title: string; + description: string; + credentialtype: string; label: string; } -export interface DataSourceEditPageItem { +export interface CreateDataSourceFormType { + title: string; + description: string; + endpoint: string; + credentialId: string; + credentialType: string; + newCredential?: CreateNewCredentialType; +} + +export interface EditDataSourceFormType { id: string; title: string; description: string; endpoint: string; credentialId: string; - noAuthentication: boolean; + credentialType: string; } export interface ToastMessageItem { @@ -62,3 +75,21 @@ export interface ToastMessageItem { export type DataSourceManagementContextValue = OpenSearchDashboardsReactContextValue< DataSourceManagementContext >; + +export enum CredentialSourceType { + CreateCredential = 'createCredential', + ExistingCredential = 'existingCredential', + NoAuth = 'noAuth', +} + +export interface CreateNewCredentialType { + title: string; + description: string; + credentialMaterials: { + credentialMaterialsType: string; + credentialMaterialsContent: { + username: string; + password: string; + }; + }; +}