From e7f0c978eda96876f3be3957d45caf4a47867a34 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 16:45:59 -0700 Subject: [PATCH] [MD] UX updates on data source page & validation changes (#2521) (#2546) * new ux changes v2.4.0 Signed-off-by: mpabba3003 * new UX changes and Validation changed Signed-off-by: mpabba3003 * refactoring duplicate title validation on edit data source Signed-off-by: mpabba3003 * replacing eui toasts with notifications toasts Signed-off-by: mpabba3003 * updating testcases after updating toasts methods Signed-off-by: mpabba3003 * adding change log for data source Signed-off-by: mpabba3003 * adding password validation on update stored password modal Signed-off-by: mpabba3003 * removing un-used text content Signed-off-by: mpabba3003 Signed-off-by: mpabba3003 (cherry picked from commit 21a173b6d3e323038eafce121dffae28d0362299) Co-authored-by: Manideep Pabba <109986843+mpabba3003@users.noreply.github.com> --- CHANGELOG.md | 1 + .../__snapshots__/create_button.test.tsx.snap | 7 +- .../create_button/create_button.tsx | 8 +- .../create_data_source_wizard.test.tsx.snap | 317 +-- .../create_data_source_form.test.tsx.snap | 2409 +++-------------- .../create_data_source_form.test.tsx | 47 +- .../create_form/create_data_source_form.tsx | 182 +- .../components/header/header.tsx | 6 +- .../create_data_source_wizard.test.tsx | 57 +- .../create_data_source_wizard.tsx | 83 +- .../data_source_table.test.tsx.snap | 2301 ++++++++-------- .../data_source_table.test.tsx | 24 +- .../data_source_table/data_source_table.tsx | 162 +- .../edit_form/edit_data_source_form.tsx | 328 ++- .../components/header/header.tsx | 28 +- .../update_password_modal.tsx | 105 +- .../edit_data_source/edit_data_source.tsx | 73 +- .../components/text_content/text_content.ts | 203 +- .../datasource_form_validation.test.ts | 75 +- .../validation/datasource_form_validation.ts | 107 +- .../data_source_management/public/plugin.ts | 1 + .../data_source_management/public/types.ts | 12 +- .../management_sidebar_nav.scss | 4 + .../management_sidebar_nav.tsx | 23 +- src/plugins/management/public/types.ts | 1 + .../public/utils/management_item.ts | 12 +- 26 files changed, 2233 insertions(+), 4343 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b792309749..f1d6dc1a692b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * Add updated_at column to objects' tables ([#1218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1218)) * [Viz Builder] State validation before dispatching and loading ([#2351](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2351)) * [Multi DataSource] UX enhacement on index pattern management stack ([#2505]https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2505)) +* [Multi DataSource] UX enhancement on Data source management stack ([#2521](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2521)) ### 🐛 Bug Fixes diff --git a/src/plugins/data_source_management/public/components/create_button/__snapshots__/create_button.test.tsx.snap b/src/plugins/data_source_management/public/components/create_button/__snapshots__/create_button.test.tsx.snap index 9cda669ba751..ce57668cd417 100644 --- a/src/plugins/data_source_management/public/components/create_button/__snapshots__/create_button.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/create_button/__snapshots__/create_button.test.tsx.snap @@ -4,13 +4,8 @@ exports[`CreateButton should render normally 1`] = ` - + Create data source connection `; 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 f5fca8b27892..b02d3716cbf5 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 @@ -7,7 +7,7 @@ import React from 'react'; import { History } from 'history'; import { EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; +import { CREATE_DATA_SOURCE_BUTTON_TEXT } from '../text_content'; interface Props { history: History; @@ -19,12 +19,8 @@ export const CreateButton = ({ history }: Props) => { data-test-subj="createDataSourceButton" fill={true} onClick={() => history.push('/create')} - iconType="plusInCircle" > - + {CREATE_DATA_SOURCE_BUTTON_TEXT} ); }; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap b/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap index cbdcf40a6478..756b3b38c7bc 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap @@ -31,6 +31,14 @@ exports[`Datasource Management: Create Datasource Wizard should render normally match={Object {}} > @@ -163,8 +171,6 @@ exports[`Datasource Management: Create Datasource Wizard should render normally
- - - } - isOpen={false} - panelPaddingSize="none" +
- +
+ +
+ +
+ + - +
+
-
- - - + Username & Password + +
+ +
+
@@ -985,24 +853,25 @@ exports[`Datasource Management: Create Datasource Wizard should render normally - -
- - - - - -
-
- - - - - - - - - - - - + Username & Password + + + + + @@ -775,6 +636,7 @@ exports[`Datasource Management: Create Datasource form should create data source
- - - } - isOpen={false} - panelPaddingSize="none" - > - + +
+ +
+ +
+ + - +
+
-
- - - + Username & Password + +
+
+
+ @@ -1834,6 +1557,7 @@ exports[`Datasource Management: Create Datasource form should create data source @@ -2028,8 +1753,6 @@ exports[`Datasource Management: Create Datasource form should render normally 1`
- - - } - isOpen={false} - panelPaddingSize="none" - > - + +
+ +
+ +
+ + - +
+
-
- - - + Username & Password + +
+
+
+ @@ -2850,24 +2435,25 @@ exports[`Datasource Management: Create Datasource form should render normally 1` - -
- - - - - -
-
- - - - - - - - - - - - + Username & Password + + + + + @@ -3945,1197 +3330,25 @@ exports[`Datasource Management: Create Datasource form should throw validation e - - - -
- - -
-
- -`; - -exports[`Datasource Management: Create Datasource form should validate when submit button is clicked without any user input on any field 1`] = ` - - - - -
-
- -
- -
-
- -

- Create data source connection -

-
- -
- - -
-

- - - A data source is an OpenSearch cluster endpoint (for now) to query against. - - -
- - - - - Read documentation - - - - - - - - - (opens in a new tab or window) - - - - - -

-
-
-
-
- -
- -
- -
-
- -
- - -
-
- - Please address the highlighted errors. - -
- -
- -
-
    -
  • - Title must not be empty -
  • -
  • - Endpoint is not valid -
  • -
  • - Username should not be empty -
  • -
  • - Password should not be empty -
  • -
-
-
-
-
-
-
-
- -
-
- - - Connection Details - - -
-
-
- -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- Title must not be empty -
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- Endpoint is not valid -
-
-
-
-
- -
- - -
-
- - - Authentication - - -
-
-
- -
- - -
-
- - - -
-
- - - } - isOpen={false} - panelPaddingSize="none" - > - - [Function] - - } - buttonRef={[Function]} - className="euiInputPopover euiSuperSelect" - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - panelRef={[Function]} - > - -
-
- -
- - - -
-
- - - - Select an option: Username & Password, is selected - - - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- Username should not be empty -
-
-
-
-
- -
-
- - - -
-
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
- -
- Password should not be empty -
-
-
-
-
- -
- - - - + +
+ +
- No items found + alpha test datasource
- - - - -
-
- -
- -
- - - -
- - - -`; - -exports[`DataSourceTable should get datasources successful should render normally 1`] = ` - - - - -
- -
- -
- -

- Data Sources -

-
- -
- - -
-

- Create and manage the data sources that help you retrieve your data from multiple Elasticsearch clusters -

-
-
-
- - -
- - - - - - - -
-
-
- - -
- - - - Delete - - connection - - - , - } - } - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "title", - }, - } - } - tableLayout="fixed" - > -
- - - Delete - - connection - - - - } - > - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
- -
- -
- - - - - -
-
-
-
-
-
-
- -
- - -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- + + -
- + + - - +
+ +
+
+ + +
+ + + + +
-
+
+ -
- - - -
-
- -
- -
- -
- -
- - - - - - - - - - - + + + + + - - + + + + + + + + + + + + - + + - - - - - - - - - - + + + + + + @@ -2177,6 +1753,365 @@ exports[`DataSourceTable should get datasources successful should render normall
- -
+ + + + + + + + -
- - -
+
+ + +
+
+ + -
-
-
-
-
- - - -
+ +
+
+ + + + + + - -
+
+ + test datasource + +
+
+ +
+ + +
+ +
+
+ + +
+
- No items found + test datasource2
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+
@@ -2184,18 +2119,6 @@ exports[`DataSourceTable should get datasources successful should render normall
- -
- `; diff --git a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx index 282e8878b629..cb30ffdc7a0b 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx @@ -16,7 +16,6 @@ import { OpenSearchDashboardsContextProvider } from '../../../../opensearch_dash import { getMappedDataSources, mockManagementPlugin } from '../../mocks'; const deleteButtonIdentifier = '[data-test-subj="deleteDataSourceConnections"]'; -const toastsIdentifier = 'EuiGlobalToastList'; const tableIdentifier = 'EuiInMemoryTable'; const confirmModalIndentifier = 'EuiConfirmModal'; const tableColumnHeaderIdentifier = 'EuiTableHeaderCell'; @@ -26,7 +25,6 @@ describe('DataSourceTable', () => { const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); let component: ReactWrapper, React.Component<{}, {}, any>>; const history = (scopedHistoryMock.create() as unknown) as ScopedHistory; - describe('should get datasources failed', () => { beforeEach(async () => { spyOn(utils, 'getDataSources').and.returnValue(Promise.reject({})); @@ -48,24 +46,6 @@ describe('DataSourceTable', () => { ); }); }); - - it('should show toast and remove toast normally', () => { - expect(component).toMatchSnapshot(); - expect(utils.getDataSources).toHaveBeenCalled(); - component.update(); - // @ts-ignore - expect(component.find(toastsIdentifier).props().toasts.length).toBe(1); - - act(() => { - // @ts-ignore - component.find(toastsIdentifier).first().prop('dismissToast')({ - id: 'dataSourcesManagement.dataSourceListing.fetchDataSourceFailMsg', - }); - }); - component.update(); - // @ts-ignore - expect(component.find(toastsIdentifier).props().toasts.length).toBe(0); // failure toast - }); }); describe('should get datasources successful', () => { @@ -88,6 +68,7 @@ describe('DataSourceTable', () => { } ); }); + component.update(); }); it('should render normally', () => { @@ -153,7 +134,7 @@ describe('DataSourceTable', () => { expect(component.find(confirmModalIndentifier).exists()).toBe(false); }); - it('should show toast when delete datasources failed', async () => { + it('should delete datasources & fail', async () => { spyOn(utils, 'deleteMultipleDataSources').and.returnValue(Promise.reject({})); act(() => { // @ts-ignore @@ -171,7 +152,6 @@ describe('DataSourceTable', () => { component.update(); expect(utils.deleteMultipleDataSources).toHaveBeenCalled(); // @ts-ignore - expect(component.find(toastsIdentifier).props().toasts.length).toBe(1); expect(component.find(confirmModalIndentifier).exists()).toBe(false); }); }); 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 97551c62ee36..f4f537436e05 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 @@ -9,18 +9,17 @@ import { EuiConfirmModal, EuiFlexGroup, EuiFlexItem, - EuiGlobalToastList, - EuiGlobalToastListToast, EuiInMemoryTable, EuiPageContent, + EuiPanel, EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; import React, { useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { FormattedMessage } from '@osd/i18n/react'; import { useEffectOnce } from 'react-use'; +import { i18n } from '@osd/i18n'; import { getListBreadcrumbs } from '../breadcrumbs'; import { reactRouterNavigate, @@ -31,16 +30,17 @@ import { CreateButton } from '../create_button'; import { deleteMultipleDataSources, getDataSources } from '../utils'; import { LoadingMask } from '../loading_mask'; import { - cancelText, - deleteText, - dsListingAriaRegion, - dsListingDeleteDataSourceConfirmation, - dsListingDeleteDataSourceDescription, - dsListingDeleteDataSourceTitle, - dsListingDeleteDataSourceWarning, - dsListingDescription, - dsListingPageTitle, - dsListingTitle, + CANCEL_TEXT, + DELETE_TEXT, + DS_LISTING_ARIA_REGION, + DS_LISTING_DATA_SOURCE_DELETE_ACTION, + DS_LISTING_DATA_SOURCE_DELETE_IMPACT, + DS_LISTING_DATA_SOURCE_DELETE_WARNING, + DS_LISTING_DATA_SOURCE_MULTI_DELETE_TITLE, + DS_LISTING_DESCRIPTION, + DS_LISTING_NO_DATA, + DS_LISTING_PAGE_TITLE, + DS_LISTING_TITLE, } from '../text_content/text_content'; /* Table config */ @@ -56,17 +56,17 @@ const sorting = { }, }; -const toastLifeTimeMs = 6000; - export const DataSourceTable = ({ history }: RouteComponentProps) => { - const { chrome, setBreadcrumbs, savedObjects } = useOpenSearchDashboards< - DataSourceManagementContext - >().services; + const { + chrome, + setBreadcrumbs, + savedObjects, + notifications: { toasts }, + } = useOpenSearchDashboards().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); @@ -77,7 +77,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { setBreadcrumbs(getListBreadcrumbs()); /* Browser - Page Title */ - chrome.docTitle.change(dsListingPageTitle); + chrome.docTitle.change(DS_LISTING_PAGE_TITLE); /* fetch data sources*/ fetchDataSources(); @@ -93,10 +93,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { setDataSources([]); handleDisplayToastMessage({ id: 'dataSourcesManagement.dataSourceListing.fetchDataSourceFailMsg', - defaultMessage: - 'Error occurred while fetching the records for Data sources. Please try it again', - color: 'warning', - iconType: 'alert', + defaultMessage: 'Error occurred while fetching the records for Data sources.', }); }) .finally(() => { @@ -109,14 +106,13 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { return ( { setConfirmDeleteVisible(true); }} data-test-subj="deleteDataSourceConnections" disabled={selectedDataSources.length === 0} > - Delete {selectedDataSources.length || ''} connection + Delete {selectedDataSources.length || ''} {selectedDataSources.length ? 'connection' : ''} {selectedDataSources.length >= 2 ? 's' : ''} ); @@ -180,7 +176,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { const tableRenderDeleteModal = () => { return confirmDeleteVisible ? ( { setConfirmDeleteVisible(false); }} @@ -188,13 +184,13 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { setConfirmDeleteVisible(false); onClickDelete(); }} - cancelButtonText={cancelText} - confirmButtonText={deleteText} + cancelButtonText={CANCEL_TEXT} + confirmButtonText={DELETE_TEXT} defaultFocusedButton="confirm" > -

{dsListingDeleteDataSourceDescription}

-

{dsListingDeleteDataSourceConfirmation}

-

{dsListingDeleteDataSourceWarning}

+

{DS_LISTING_DATA_SOURCE_DELETE_ACTION}

+

{DS_LISTING_DATA_SOURCE_DELETE_IMPACT}

+

{DS_LISTING_DATA_SOURCE_DELETE_WARNING}

) : null; }; @@ -214,9 +210,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { 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', + 'Error occurred while deleting selected records for Data sources. Please try it again', }); }) .finally(() => { @@ -234,21 +228,13 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { }; /* Toast Handlers */ - const removeToast = (id: string) => { - setToasts(toasts.filter((toast) => toast.id !== id)); - }; - const handleDisplayToastMessage = ({ id, defaultMessage, color, iconType }: ToastMessageItem) => { - const failureMsg = ; - setToasts([ - ...toasts, - { - title: failureMsg, - id: failureMsg.props.id, - color, - iconType, - }, - ]); + const handleDisplayToastMessage = ({ id, defaultMessage }: ToastMessageItem) => { + toasts.addWarning( + i18n.translate(id, { + defaultMessage, + }) + ); }; /* Render Ui elements*/ @@ -261,11 +247,11 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { -

{dsListingTitle}

+

{DS_LISTING_TITLE}

-

{dsListingDescription}

+

{DS_LISTING_DESCRIPTION}

{createButton} @@ -275,12 +261,46 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { /* Render table */ const renderTableContent = () => { + return ( + <> + {/* Data sources table*/} + + + ); + }; + + const renderEmptyState = () => { + return ( + <> + + + {DS_LISTING_NO_DATA} + + {createButton} + + + + ); + }; + + const renderContent = () => { return ( <> {/* Header */} {renderHeader()} @@ -290,46 +310,16 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { {/* Delete confirmation modal*/} {tableRenderDeleteModal()} - {/* Data sources table*/} - + {!isLoading && (!dataSources || !dataSources.length) + ? renderEmptyState() + : renderTableContent()} {isDeleting ? : null} ); }; - const renderContent = () => { - return ( - <> - {renderTableContent()} - {} - - ); - }; - - return ( - <> - {renderContent()} - { - removeToast(id); - }} - toastLifeTimeMs={toastLifeTimeMs} - /> - - ); + return renderContent(); }; export const DataSourceTableWithRouter = withRouter(DataSourceTable); 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 index bf59853d5427..3edc2b1b7371 100644 --- 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 @@ -16,17 +16,15 @@ import { EuiFormRow, EuiHorizontalRule, EuiPanel, + EuiRadioGroup, EuiSpacer, - EuiSuperSelect, EuiText, - EuiToolTip, } from '@elastic/eui'; import { AuthType, credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, - UpdatePasswordFormType, UsernamePasswordTypedContent, } from '../../../../types'; import { Header } from '../header'; @@ -34,40 +32,40 @@ import { context as contextType } from '../../../../../../opensearch_dashboards_ import { CreateEditDataSourceValidation, defaultValidation, + isTitleValid, performDataSourceFormValidation, } from '../../../validation'; import { UpdatePasswordModal } from '../update_password_modal'; import { - authenticationDetailsDescription, - authenticationDetailsText, - authenticationMethodTitle, - authenticationTitle, - cancelChangesText, - connectionDetailsText, - createDataSourceDescriptionPlaceholder, - createDataSourceEndpointURL, - createDataSourcePasswordPlaceholder, - createDataSourceUsernamePlaceholder, - descriptionText, - endpointDescription, - endpointTitle, - objectDetailsDescription, - objectDetailsText, - passwordText, - saveChangesText, - titleText, - updatePasswordText, - usernameText, - validationErrorTooltipText, + AUTHENTICATION_METHOD, + AUTHENTICATION_TITLE, + CANCEL_CHANGES, + CONNECTION_DETAILS_TITLE, + DATA_SOURCE_DESCRIPTION_PLACEHOLDER, + DATA_SOURCE_PASSWORD_PLACEHOLDER, + USERNAME_PLACEHOLDER, + CREDENTIAL, + DESCRIPTION, + ENDPOINT_DESCRIPTION, + ENDPOINT_TITLE, + ENDPOINT_URL, + OBJECT_DETAILS_DESCRIPTION, + OBJECT_DETAILS_TITLE, + OPTIONAL, + PASSWORD, + SAVE_CHANGES, + TITLE, + UPDATE_STORED_PASSWORD, + USERNAME, } from '../../../text_content'; export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; + existingDatasourceNamesList: string[]; handleSubmit: (formValues: DataSourceAttributes) => void; onDeleteDataSource?: () => void; } export interface EditDataSourceState { - formErrors: string[]; formErrorsByField: CreateEditDataSourceValidation; title: string; description: string; @@ -78,9 +76,6 @@ export interface EditDataSourceState { }; showUpdatePasswordModal: boolean; showUpdateOptions: boolean; - oldPassword: string; - newPassword: string; - confirmNewPassword: string; } export class EditDataSourceForm extends React.Component { @@ -92,7 +87,6 @@ export class EditDataSourceForm extends React.Component { this.setFormValuesForEditMode(); - this.setState({ showUpdateOptions: false }, this.checkValidation); + this.setState({ showUpdateOptions: false }); }; setFormValuesForEditMode() { @@ -143,30 +134,49 @@ export class EditDataSourceForm extends React.Component { - const { formErrors, formErrorsByField } = performDataSourceFormValidation(this.state); - - this.setState({ - formErrors, - formErrorsByField, - }); - - return formErrors.length === 0; + return performDataSourceFormValidation( + this.state, + this.props.existingDatasourceNamesList, + this.props.existingDataSource.title + ); }; /* Events */ onChangeTitle = (e: { target: { value: any } }) => { - this.setState({ title: e.target.value }, () => { - if (this.state.formErrorsByField.title.length) { - this.isFormValid(); - } + this.setState({ title: e.target.value }); + }; + + validateTitle = () => { + const isValid = isTitleValid( + this.state.title, + this.props.existingDatasourceNamesList, + this.props.existingDataSource.title + ); + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + title: isValid.valid ? [] : [isValid.error], + }, }); }; - onChangeAuthType = (value: AuthType) => { - this.setState({ auth: { ...this.state.auth, type: value } }, () => { + onChangeAuthType = (value: string) => { + const valueToSave = + value === AuthType.UsernamePasswordType ? AuthType.UsernamePasswordType : AuthType.NoAuth; + + const formErrorsByField = { + ...this.state.formErrorsByField, + createCredential: { ...this.state.formErrorsByField.createCredential }, + }; + if (valueToSave === AuthType.NoAuth) { + formErrorsByField.createCredential = { + username: [], + password: [], + }; + } + this.setState({ auth: { ...this.state.auth, type: valueToSave }, formErrorsByField }, () => { this.onChangeFormValues(); - this.checkValidation(); }); }; @@ -175,33 +185,46 @@ export class EditDataSourceForm extends React.Component { - this.setState( - { - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, username: e.target.value }, + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, username: e.target.value }, + }, + }); + }; + validateUsername = () => { + const isValid = !!this.state.auth.credentials.username?.trim().length; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + createCredential: { + ...this.state.formErrorsByField.createCredential, + username: isValid ? [] : [''], }, }, - this.checkValidation - ); + }); }; - onChangePassword = (e: { target: { value: any } }) => { - this.setState( - { - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, password: e.target.value }, + validatePassword = () => { + const isValid = !!this.state.auth.credentials.password; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + createCredential: { + ...this.state.formErrorsByField.createCredential, + password: isValid ? [] : [''], }, }, - this.checkValidation - ); + }); }; - checkValidation = () => { - if (this.state.formErrors.length) { - this.isFormValid(); - } + onChangePassword = (e: { target: { value: any } }) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, password: e.target.value }, + }, + }); }; onClickUpdateDataSource = () => { @@ -242,8 +265,22 @@ export class EditDataSourceForm extends React.Component { - // TODO: update password when API is ready + /* Update password */ + updatePassword = (password: string) => { + const { title, description, auth } = this.props.existingDataSource; + const updateAttributes: DataSourceAttributes = { + title, + description, + endpoint: undefined, + auth: { + type: auth.type, + credentials: { + username: auth.credentials ? auth.credentials.username : '', + password, + }, + }, + }; + this.props.handleSubmit(updateAttributes); this.closePasswordModal(); }; @@ -257,10 +294,11 @@ export class EditDataSourceForm extends React.Component { return ( <> - {updatePasswordText} + {UPDATE_STORED_PASSWORD} {this.state.showUpdatePasswordModal ? ( @@ -279,38 +317,50 @@ export class EditDataSourceForm extends React.Component { + return ( + <> + {label} - {OPTIONAL} + + ); + }; + /* Render Connection Details Panel */ renderConnectionDetailsSection = () => { return ( - {connectionDetailsText} + +

{CONNECTION_DETAILS_TITLE}

+
{objectDetailsText}} - description={

{objectDetailsDescription}

} + title={

{OBJECT_DETAILS_TITLE}

} + description={

{OBJECT_DETAILS_DESCRIPTION}

} > {/* Title */} {/* Description */} - + @@ -323,16 +373,18 @@ export class EditDataSourceForm extends React.Component { return ( - {endpointTitle} + +

{ENDPOINT_TITLE}

+
{createDataSourceEndpointURL}} - description={

{endpointDescription}

} + title={

{ENDPOINT_URL}

} + description={

{ENDPOINT_DESCRIPTION}

} > {/* Endpoint */} - + { return ( - {authenticationTitle} + +

{AUTHENTICATION_TITLE}

+
- {authenticationDetailsText}} - description={

{authenticationDetailsDescription}

} - > + {AUTHENTICATION_METHOD}}> {this.renderCredentialsSection()}
@@ -368,11 +419,12 @@ export class EditDataSourceForm extends React.Component {/* Auth type select */} - - + this.onChangeAuthType(value)} + idSelected={this.state.auth.type} + onChange={(id) => this.onChangeAuthType(id)} + name="Credential" /> @@ -387,47 +439,48 @@ export class EditDataSourceForm extends React.Component {/* Username */} - + {/* Password */} - {this.props.existingDataSource.auth.type === AuthType.NoAuth - ? this.renderEmptyPasswordField() - : this.renderDisabledPasswordField()} + + + + + + {this.props.existingDataSource.auth.type !== AuthType.NoAuth ? ( + {this.renderUpdatePasswordModal()} + ) : null} + + ); }; - renderDisabledPasswordField = () => { - return ( - - - - ************* - - {this.renderUpdatePasswordModal()} - - - ); - }; - - renderEmptyPasswordField = () => { - return ( - - - - ); - }; - didFormValuesChange = () => { const formValues: DataSourceAttributes = { title: this.state.title, @@ -478,24 +531,22 @@ export class EditDataSourceForm extends React.Component - {cancelChangesText} + {CANCEL_CHANGES} - - - {saveChangesText} - - + + {SAVE_CHANGES} +
@@ -508,12 +559,7 @@ export class EditDataSourceForm extends React.Component {this.renderHeader()} - this.onChangeFormValues()} - data-test-subj="data-source-edit" - isInvalid={!!this.state.formErrors.length} - error={this.state.formErrors} - > + this.onChangeFormValues()} data-test-subj="data-source-edit"> {this.renderConnectionDetailsSection()} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx index f9d957f090f0..974e29ff0635 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx @@ -18,13 +18,12 @@ import { import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; import { DataSourceManagementContext } from '../../../../types'; import { - cancelText, - deleteText, - deleteThisDataSource, - dsListingDeleteDataSourceConfirmation, - dsListingDeleteDataSourceDescription, - dsListingDeleteDataSourceTitle, - dsListingDeleteDataSourceWarning, + CANCEL_TEXT, + DELETE_TEXT, + DS_LISTING_DATA_SOURCE_DELETE_IMPACT, + DS_LISTING_DATA_SOURCE_DELETE_WARNING, + DELETE_THIS_DATA_SOURCE, + DS_UPDATE_DATA_SOURCE_DELETE_TITLE, } from '../../../text_content'; export const Header = ({ @@ -47,7 +46,7 @@ export const Header = ({ const renderDeleteButton = () => { return ( <> - + { @@ -56,13 +55,13 @@ export const Header = ({ iconType="trash" iconSize="m" size="m" - aria-label={deleteThisDataSource} + aria-label={DELETE_THIS_DATA_SOURCE} /> {isDeleteModalVisible ? ( { setIsDeleteModalVisible(false); }} @@ -70,13 +69,12 @@ export const Header = ({ setIsDeleteModalVisible(false); onClickDeleteIcon(); }} - cancelButtonText={cancelText} - confirmButtonText={deleteText} + cancelButtonText={CANCEL_TEXT} + confirmButtonText={DELETE_TEXT} defaultFocusedButton="confirm" > -

{dsListingDeleteDataSourceDescription}

-

{dsListingDeleteDataSourceConfirmation}

-

{dsListingDeleteDataSourceWarning}

+

{DS_LISTING_DATA_SOURCE_DELETE_IMPACT}

+

{DS_LISTING_DATA_SOURCE_DELETE_WARNING}

) : null} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx index df85ce1fd9af..a77e67f148ad 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -15,125 +15,66 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, + EuiText, } from '@elastic/eui'; -import { UpdatePasswordFormType } from '../../../../types'; -import { - defaultPasswordValidationByField, - UpdatePasswordValidation, - validateUpdatePassword, -} from '../../../validation'; -import { confirmNewPasswordText, newPasswordText, oldPasswordText } from '../../../text_content'; +import { NEW_PASSWORD_TEXT, UPDATE_STORED_PASSWORD, USERNAME } from '../../../text_content'; export interface UpdatePasswordModalProps { - handleUpdatePassword: (passwords: UpdatePasswordFormType) => void; + username: string; + handleUpdatePassword: (password: string) => void; closeUpdatePasswordModal: () => void; } export const UpdatePasswordModal = ({ + username, handleUpdatePassword, closeUpdatePasswordModal, }: UpdatePasswordModalProps) => { /* State Variables */ - const [formErrors, setFormErrors] = useState([]); - const [formErrorsByField, setFormErrorsByField] = useState( - defaultPasswordValidationByField - ); - const [oldPassword, setOldPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); - const [confirmNewPassword, setConfirmNewPassword] = useState(''); - - const getFormValues = useCallback(() => { - return { - oldPassword, - newPassword, - confirmNewPassword, - }; - }, [oldPassword, newPassword, confirmNewPassword]); const onClickUpdatePassword = () => { - if (isFormValid()) { - handleUpdatePassword(getFormValues()); + if (!!newPassword) { + handleUpdatePassword(newPassword); } }; - /* Validations */ - const isFormValid = useCallback(() => { - const { formValidationErrors, formValidationErrorsByField } = validateUpdatePassword( - getFormValues() - ); - - setFormErrors([...formValidationErrors]); - setFormErrorsByField({ ...formValidationErrorsByField }); - - return formValidationErrors.length === 0; - }, [getFormValues]); - - useEffect(() => { - if (formErrors.length) { - isFormValid(); - } - }, [oldPassword, newPassword, confirmNewPassword, formErrors.length, isFormValid]); - const renderUpdatePasswordModal = () => { return ( -

Update password

+

{UPDATE_STORED_PASSWORD}

- - - setOldPassword(e.target.value)} - /> + + {/* Username */} + + {username} - + {/* Password */} + setNewPassword(e.target.value)} /> - - setConfirmNewPassword(e.target.value)} - /> - Cancel - + Update 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 0d27d650c273..e502adc5d5ca 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 @@ -6,16 +6,21 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import React, { useState } from 'react'; import { useEffectOnce } from 'react-use'; -import { EuiGlobalToastList, EuiGlobalToastListToast } from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; +import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { DataSourceManagementContext, ToastMessageItem } from '../../types'; -import { deleteDataSourceById, getDataSourceById, updateDataSourceById } from '../utils'; +import { DataSourceManagementContext, DataSourceTableItem, ToastMessageItem } from '../../types'; +import { + deleteDataSourceById, + getDataSourceById, + getDataSources, + updateDataSourceById, +} from '../utils'; import { getEditBreadcrumbs } from '../breadcrumbs'; import { EditDataSourceForm } from './components/edit_form/edit_data_source_form'; import { LoadingMask } from '../loading_mask'; import { AuthType, DataSourceAttributes } from '../../types'; -import { dataSourceNotFound } from '../text_content'; +import { DATA_SOURCE_NOT_FOUND } from '../text_content'; const defaultDataSource: DataSourceAttributes = { title: '', @@ -31,17 +36,17 @@ const EditDataSource: React.FunctionComponent ) => { /* Initialization */ - const { savedObjects, setBreadcrumbs } = useOpenSearchDashboards< - DataSourceManagementContext - >().services; + const { + savedObjects, + setBreadcrumbs, + notifications: { toasts }, + } = useOpenSearchDashboards().services; const dataSourceID: string = props.match.params.id; /* State Variables */ const [dataSource, setDataSource] = useState(defaultDataSource); + const [existingDatasourceNamesList, setExistingDatasourceNamesList] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [toasts, setToasts] = useState([]); - - const toastLifeTimeMs: number = 6000; /* Fetch data source by id*/ useEffectOnce(() => { @@ -49,18 +54,21 @@ const EditDataSource: React.FunctionComponent datasource.title?.toLowerCase()) + ); + } } catch (e) { handleDisplayToastMessage({ id: 'dataSourcesManagement.editDataSource.editDataSourceFailMsg', - defaultMessage: 'Unable to find the Data Source. Please try it again.', - color: 'warning', - iconType: 'alert', + defaultMessage: 'Unable to find the Data Source.', }); - props.history.push(''); } finally { setIsLoading(false); @@ -79,25 +87,12 @@ const EditDataSource: React.FunctionComponent { - if (id && defaultMessage && color && iconType) { - const failureMsg = ; - setToasts([ - ...toasts, - { - title: failureMsg, - id: failureMsg.props.id, - color, - iconType, - }, - ]); - } + const handleDisplayToastMessage = ({ id, defaultMessage }: ToastMessageItem) => { + toasts.addWarning(i18n.translate(id, { defaultMessage })); }; /* Handle delete - data source*/ @@ -111,8 +106,6 @@ const EditDataSource: React.FunctionComponent @@ -136,25 +130,14 @@ const EditDataSource: React.FunctionComponent { - setToasts(toasts.filter((toast) => toast.id !== id)); - }; - if (!isLoading && !dataSource?.endpoint) { - return

{dataSourceNotFound}

; + return

{DATA_SOURCE_NOT_FOUND}

; } return ( <> + {renderContent()} - { - removeToast(id); - }} - toastLifeTimeMs={toastLifeTimeMs} - /> ); }; diff --git a/src/plugins/data_source_management/public/components/text_content/text_content.ts b/src/plugins/data_source_management/public/components/text_content/text_content.ts index ee6bca828eb0..56a2ea93be15 100644 --- a/src/plugins/data_source_management/public/components/text_content/text_content.ts +++ b/src/plugins/data_source_management/public/components/text_content/text_content.ts @@ -6,123 +6,141 @@ import { i18n } from '@osd/i18n'; /* Generic */ -export const cancelText = i18n.translate('cancel', { +export const CANCEL_TEXT = i18n.translate('cancel', { defaultMessage: 'Cancel', }); -export const deleteText = i18n.translate('delete', { +export const DELETE_TEXT = i18n.translate('delete', { defaultMessage: 'Delete', }); -export const titleText = i18n.translate('title', { +export const TITLE = i18n.translate('title', { defaultMessage: 'Title', }); -export const descriptionText = i18n.translate('description', { +export const DESCRIPTION = i18n.translate('description', { defaultMessage: 'Description', }); -export const usernameText = i18n.translate('username', { +export const OPTIONAL = i18n.translate('optional', { + defaultMessage: 'optional', +}); + +export const USERNAME = i18n.translate('username', { defaultMessage: 'Username', }); -export const passwordText = i18n.translate('password', { +export const PASSWORD = i18n.translate('password', { defaultMessage: 'Password', }); /* Datasource listing page */ -export const dsListingAriaRegion = i18n.translate( +export const DS_LISTING_ARIA_REGION = i18n.translate( 'dataSourcesManagement.createDataSourcesLiveRegionAriaLabel', { defaultMessage: 'Data Sources', } ); -export const dsListingTitle = i18n.translate('dataSourcesManagement.dataSourcesTable.title', { +export const DS_LISTING_TITLE = i18n.translate('dataSourcesManagement.dataSourcesTable.title', { defaultMessage: 'Data Sources', }); -export const dsListingDescription = i18n.translate( +export const DS_LISTING_DESCRIPTION = i18n.translate( 'dataSourcesManagement.dataSourcesTable.description', { defaultMessage: - 'Create and manage the data sources that help you retrieve your data from multiple Elasticsearch clusters', + 'Create and manage data source connections to help you retrieve data from multiple OpenSearch compatible sources.', } ); -export const dsListingPageTitle = i18n.translate( +export const DS_LISTING_PAGE_TITLE = i18n.translate( 'dataSourcesManagement.dataSourcesTable.dataSourcesTitle', { defaultMessage: 'Data Sources', } ); -export const dsListingDeleteDataSourceTitle = i18n.translate( - 'dataSourcesManagement.dataSourcesTable.deleteTitle', +export const DS_LISTING_NO_DATA = i18n.translate('dataSourcesManagement.dataSourcesTable.noData', { + defaultMessage: 'No Data Source Connections have been created yet.', +}); + +export const DS_LISTING_DATA_SOURCE_MULTI_DELETE_TITLE = i18n.translate( + 'dataSourcesManagement.dataSourcesTable.multiDeleteTitle', + { + defaultMessage: 'Delete data source connection(s)', + } +); + +export const DS_UPDATE_DATA_SOURCE_DELETE_TITLE = i18n.translate( + 'dataSourcesManagement.dataSourcesUpdate.deleteTitle', { - defaultMessage: 'Delete Data Source connection(s) permanently?', + defaultMessage: 'Delete data source connection', } ); -export const dsListingDeleteDataSourceDescription = i18n.translate( +export const DS_LISTING_DATA_SOURCE_DELETE_ACTION = i18n.translate( 'dataSourcesManagement.dataSourcesTable.deleteDescription', { - defaultMessage: - 'This will delete data source connections(s) and all Index Patterns using this credential will be invalid for access.', + defaultMessage: 'This action will delete the selected data source connections', } ); -export const dsListingDeleteDataSourceConfirmation = i18n.translate( +export const DS_LISTING_DATA_SOURCE_DELETE_IMPACT = i18n.translate( 'dataSourcesManagement.dataSourcesTable.deleteConfirmation', { - defaultMessage: 'To confirm deletion, click delete button.', + defaultMessage: + 'Any objects created using data from these sources, including Index Patterns, Visualizations, and Observability Panels, will be impacted.', } ); -export const dsListingDeleteDataSourceWarning = i18n.translate( +export const DS_LISTING_DATA_SOURCE_DELETE_WARNING = i18n.translate( 'dataSourcesManagement.dataSourcesTable.deleteWarning', { - defaultMessage: 'Note: this action is irrevocable!', + defaultMessage: 'This action cannot be undone.', } ); /* CREATE DATA SOURCE */ -export const createDataSourceHeader = i18n.translate( - 'dataSourcesManagement.createDataSourceHeader', +export const CREATE_DATA_SOURCE_BUTTON_TEXT = i18n.translate( + 'dataSourcesManagement.dataSourceListing.createButton', { defaultMessage: 'Create data source connection', } ); -export const createDataSourceDescriptionPlaceholder = i18n.translate( - 'dataSourcesManagement.createDataSource.descriptionPlaceholder', +export const CREATE_DATA_SOURCE_HEADER = i18n.translate( + 'dataSourcesManagement.createDataSourceHeader', { - defaultMessage: 'Description of the data source', + defaultMessage: 'Create data source connection', } ); -export const createDataSourceEndpointURL = i18n.translate( - 'dataSourcesManagement.createDataSource.endpointURL', +export const DATA_SOURCE_DESCRIPTION_PLACEHOLDER = i18n.translate( + 'dataSourcesManagement.createDataSource.descriptionPlaceholder', { - defaultMessage: 'Endpoint URL', + defaultMessage: 'Description of the data source', } ); -export const createDataSourceEndpointPlaceholder = i18n.translate( +export const ENDPOINT_URL = i18n.translate('dataSourcesManagement.createDataSource.endpointURL', { + defaultMessage: 'Endpoint URL', +}); +export const ENDPOINT_PLACEHOLDER = i18n.translate( 'dataSourcesManagement.createDataSource.endpointPlaceholder', { defaultMessage: 'The connection URL', } ); -export const createDataSourceUsernamePlaceholder = i18n.translate( +export const USERNAME_PLACEHOLDER = i18n.translate( 'dataSourcesManagement.createDataSource.usernamePlaceholder', { defaultMessage: 'Username to connect to data source', } ); -export const createDataSourcePasswordPlaceholder = i18n.translate( +export const DATA_SOURCE_PASSWORD_PLACEHOLDER = i18n.translate( 'dataSourcesManagement.createDataSource.passwordPlaceholder', { defaultMessage: 'Password to connect to data source', } ); -export const createDataSourceCredentialSource = i18n.translate( +export const CREDENTIAL_SOURCE = i18n.translate( 'dataSourcesManagement.createDataSource.credentialSource', { defaultMessage: 'Credential Source', @@ -130,83 +148,68 @@ export const createDataSourceCredentialSource = i18n.translate( ); /* Edit data source */ -export const dataSourceNotFound = i18n.translate( +export const DATA_SOURCE_NOT_FOUND = i18n.translate( 'dataSourcesManagement.editDataSource.dataSourceNotFound', { defaultMessage: 'Data Source not found!', } ); -export const deleteThisDataSource = i18n.translate( +export const DELETE_THIS_DATA_SOURCE = i18n.translate( 'dataSourcesManagement.editDataSource.deleteThisDataSource', { defaultMessage: 'Delete this Data Source', } ); -export const oldPasswordText = i18n.translate('dataSourcesManagement.editDataSource.oldPassword', { - defaultMessage: 'Old password', -}); -export const newPasswordText = i18n.translate('dataSourcesManagement.editDataSource.newPassword', { - defaultMessage: 'New password', -}); -export const confirmNewPasswordText = i18n.translate( - 'dataSourcesManagement.editDataSource.confirmNewPassword', +export const NEW_PASSWORD_TEXT = i18n.translate( + 'dataSourcesManagement.editDataSource.newPassword', { - defaultMessage: 'Confirm new password', + defaultMessage: 'New password', } ); -export const updatePasswordText = i18n.translate( - 'dataSourcesManagement.editDataSource.updatePasswordText', +export const UPDATE_STORED_PASSWORD = i18n.translate( + 'dataSourcesManagement.editDataSource.updateStoredPassword', { - defaultMessage: 'Update password', + defaultMessage: 'Update stored password', } ); -export const connectionDetailsText = i18n.translate( +export const CONNECTION_DETAILS_TITLE = i18n.translate( 'dataSourcesManagement.editDataSource.connectionDetailsText', { defaultMessage: 'Connection Details', } ); -export const objectDetailsText = i18n.translate( +export const OBJECT_DETAILS_TITLE = i18n.translate( 'dataSourcesManagement.editDataSource.objectDetailsText', { defaultMessage: 'Object Details', } ); -export const objectDetailsDescription = i18n.translate( +export const OBJECT_DETAILS_DESCRIPTION = i18n.translate( 'dataSourcesManagement.editDataSource.objectDetailsDescription', { defaultMessage: 'This connection information is used for reference in tables and when adding to a data source connection', } ); -export const authenticationMethodTitle = i18n.translate( - 'dataSourcesManagement.editDataSource.authenticationMethodTitle', - { - defaultMessage: 'Authentication Method', - } -); -export const authenticationTitle = i18n.translate( +export const CREDENTIAL = i18n.translate('dataSourcesManagement.editDataSource.credential', { + defaultMessage: 'Credential', +}); +export const AUTHENTICATION_TITLE = i18n.translate( 'dataSourcesManagement.editDataSource.authenticationTitle', { defaultMessage: 'Authentication', } ); -export const authenticationDetailsText = i18n.translate( - 'dataSourcesManagement.editDataSource.authenticationDetailsText', +export const AUTHENTICATION_METHOD = i18n.translate( + 'dataSourcesManagement.editDataSource.authenticationMethod', { - defaultMessage: 'Authentication Details', - } -); -export const authenticationDetailsDescription = i18n.translate( - 'dataSourcesManagement.editDataSource.authenticationDetailsDescription', - { - defaultMessage: 'Modify these to update the authentication type and associated details', + defaultMessage: 'Authentication Method', } ); -export const endpointTitle = i18n.translate('dataSourcesManagement.editDataSource.endpointTitle', { +export const ENDPOINT_TITLE = i18n.translate('dataSourcesManagement.editDataSource.endpointTitle', { defaultMessage: 'Endpoint', }); -export const endpointDescription = i18n.translate( +export const ENDPOINT_DESCRIPTION = i18n.translate( 'dataSourcesManagement.editDataSource.endpointDescription', { defaultMessage: @@ -214,70 +217,20 @@ export const endpointDescription = i18n.translate( } ); -export const cancelChangesText = i18n.translate( +export const CANCEL_CHANGES = i18n.translate( 'dataSourcesManagement.editDataSource.cancelButtonLabel', { defaultMessage: 'Cancel changes', } ); -export const saveChangesText = i18n.translate( - 'dataSourcesManagement.editDataSource.saveButtonLabel', - { - defaultMessage: 'Save changes', - } -); - -export const validationErrorTooltipText = i18n.translate( - 'dataSourcesManagement.editDataSource.saveButtonTooltipWithInvalidChanges', - { - defaultMessage: 'Fix invalid settings before saving.', - } -); - -/* Password validation */ - -export const dataSourceValidationOldPasswordEmpty = i18n.translate( - 'dataSourcesManagement.validation.oldPasswordEmpty', - { - defaultMessage: 'Old password cannot be empty', - } -); -export const dataSourceValidationNewPasswordEmpty = i18n.translate( - 'dataSourcesManagement.validation.newPasswordEmpty', - { - defaultMessage: 'New password cannot be empty', - } -); -export const dataSourceValidationNoPasswordMatch = i18n.translate( - 'dataSourcesManagement.validation.noPasswordMatch', - { - defaultMessage: 'Passwords do not match', - } -); +export const SAVE_CHANGES = i18n.translate('dataSourcesManagement.editDataSource.saveButtonLabel', { + defaultMessage: 'Save changes', +}); /* Create/Edit validation */ - -export const dataSourceValidationTitleEmpty = i18n.translate( - 'dataSourcesManagement.validation.titleEmpty', - { - defaultMessage: 'Title must not be empty', - } -); -export const dataSourceValidationEndpointNotValid = i18n.translate( - 'dataSourcesManagement.validation.endpointNotValid', - { - defaultMessage: 'Endpoint is not valid', - } -); -export const dataSourceValidationUsernameEmpty = i18n.translate( - 'dataSourcesManagement.validation.usernameEmpty', - { - defaultMessage: 'Username should not be empty', - } -); -export const dataSourceValidationPasswordEmpty = i18n.translate( - 'dataSourcesManagement.validation.passwordEmpty', +export const DATA_SOURCE_VALIDATION_TITLE_EXISTS = i18n.translate( + 'dataSourcesManagement.validation.titleExists', { - defaultMessage: 'Password should not be empty', + defaultMessage: 'This title is already in use', } ); diff --git a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts index 0c64abc04878..9a00d0f29c91 100644 --- a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts +++ b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts @@ -6,17 +6,12 @@ import { AuthType } from '../../types'; import { CreateDataSourceState } from '../create_data_source_wizard/components/create_form/create_data_source_form'; import { EditDataSourceState } from '../edit_data_source/components/edit_form/edit_data_source_form'; -import { - defaultValidation, - performDataSourceFormValidation, - validateUpdatePassword, -} from './datasource_form_validation'; +import { defaultValidation, performDataSourceFormValidation } from './datasource_form_validation'; import { mockDataSourceAttributesWithAuth } from '../../mocks'; describe('DataSourceManagement: Form Validation', () => { describe('validate create/edit datasource', () => { let form: CreateDataSourceState | EditDataSourceState = { - formErrors: [], formErrorsByField: { ...defaultValidation }, title: '', description: '', @@ -29,46 +24,46 @@ describe('DataSourceManagement: Form Validation', () => { }, }, }; - test('should fail validation on all fields', () => { - const result = performDataSourceFormValidation(form); - expect(result.formErrors.length).toBe(4); + test('should fail validation when title is empty', () => { + const result = performDataSourceFormValidation(form, [], ''); + expect(result).toBe(false); + }); + test('should fail validation on duplicate title', () => { + form.title = 'test'; + const result = performDataSourceFormValidation(form, ['oldTitle', 'test'], 'oldTitle'); + expect(result).toBe(false); + }); + test('should fail validation when endpoint is not valid', () => { + form.endpoint = mockDataSourceAttributesWithAuth.endpoint; + const result = performDataSourceFormValidation(form, [], ''); + expect(result).toBe(false); + }); + test('should fail validation when username is empty', () => { + form.endpoint = 'test'; + const result = performDataSourceFormValidation(form, [], ''); + expect(result).toBe(false); + }); + test('should fail validation when password is empty', () => { + form.auth.credentials.username = 'test'; + form.auth.credentials.password = ''; + const result = performDataSourceFormValidation(form, [], ''); + expect(result).toBe(false); }); test('should NOT fail validation on empty username/password when No Auth is selected', () => { form.auth.type = AuthType.NoAuth; - const result = performDataSourceFormValidation(form); - expect(result.formErrors.length).toBe(2); - expect(result.formErrorsByField.createCredential.username.length).toBe(0); - expect(result.formErrorsByField.createCredential.password.length).toBe(0); + form.title = 'test'; + form.endpoint = mockDataSourceAttributesWithAuth.endpoint; + const result = performDataSourceFormValidation(form, [], ''); + expect(result).toBe(true); }); test('should NOT fail validation on all fields', () => { form = { ...form, ...mockDataSourceAttributesWithAuth }; - const result = performDataSourceFormValidation(form); - expect(result.formErrors.length).toBe(0); - }); - }); - - describe('validate passwords', () => { - const passwords = { - oldPassword: '', - newPassword: '', - confirmNewPassword: '', - }; - test('should fail validation for all fields', () => { - const result = validateUpdatePassword(passwords); - expect(result.formValidationErrors.length).toBe(2); - }); - test('should fail validation when passwords do not match', () => { - passwords.oldPassword = 'test'; - passwords.newPassword = 'test123'; - const result = validateUpdatePassword(passwords); - expect(result.formValidationErrors.length).toBe(1); - expect(result.formValidationErrorsByField.confirmNewPassword.length).toBe(1); - }); - test('should NOT fail validation ', () => { - passwords.confirmNewPassword = 'test123'; - const result = validateUpdatePassword(passwords); - expect(result.formValidationErrors.length).toBe(0); - expect(result.formValidationErrorsByField.confirmNewPassword.length).toBe(0); + const result = performDataSourceFormValidation( + form, + [mockDataSourceAttributesWithAuth.title], + mockDataSourceAttributesWithAuth.title + ); + expect(result).toBe(true); }); }); }); 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 index 9135f4292923..4e5a465d57fb 100644 --- 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 @@ -4,19 +4,10 @@ */ import { isValidUrl } from '../utils'; +import { DATA_SOURCE_VALIDATION_TITLE_EXISTS } from '../text_content'; import { CreateDataSourceState } from '../create_data_source_wizard/components/create_form/create_data_source_form'; -import { AuthType } from '../../types'; import { EditDataSourceState } from '../edit_data_source/components/edit_form/edit_data_source_form'; -import { UpdatePasswordFormType } from '../../types'; -import { - dataSourceValidationEndpointNotValid, - dataSourceValidationNewPasswordEmpty, - dataSourceValidationNoPasswordMatch, - dataSourceValidationOldPasswordEmpty, - dataSourceValidationPasswordEmpty, - dataSourceValidationTitleEmpty, - dataSourceValidationUsernameEmpty, -} from '../text_content'; +import { AuthType } from '../../types'; export interface CreateEditDataSourceValidation { title: string[]; @@ -27,12 +18,6 @@ export interface CreateEditDataSourceValidation { }; } -export interface UpdatePasswordValidation { - oldPassword: string[]; - newPassword: string[]; - confirmNewPassword: string[]; -} - export const defaultValidation: CreateEditDataSourceValidation = { title: [], endpoint: [], @@ -41,34 +26,46 @@ export const defaultValidation: CreateEditDataSourceValidation = { password: [], }, }; -export const defaultPasswordValidationByField: UpdatePasswordValidation = { - oldPassword: [], - newPassword: [], - confirmNewPassword: [], + +export const isTitleValid = ( + title: string, + existingDatasourceNamesList: string[], + existingTitle: string +) => { + const isValid = { + valid: true, + error: '', + }; + /* Title validation */ + if (!title?.trim?.().length) { + isValid.valid = false; + } else if ( + title.toLowerCase() !== existingTitle.toLowerCase() && + Array.isArray(existingDatasourceNamesList) && + existingDatasourceNamesList.includes(title.toLowerCase()) + ) { + /* title already exists */ + isValid.valid = false; + isValid.error = DATA_SOURCE_VALIDATION_TITLE_EXISTS; + } + return isValid; }; export const performDataSourceFormValidation = ( - formValues: CreateDataSourceState | EditDataSourceState + formValues: CreateDataSourceState | EditDataSourceState, + existingDatasourceNamesList: string[], + existingTitle: string ) => { - const validationByField: CreateEditDataSourceValidation = { - title: [], - endpoint: [], - createCredential: { - username: [], - password: [], - }, - }; - const formErrorMessages: string[] = []; /* Title validation */ - if (!formValues?.title?.trim?.().length) { - validationByField.title.push(dataSourceValidationTitleEmpty); - formErrorMessages.push(dataSourceValidationTitleEmpty); + const titleValid = isTitleValid(formValues?.title, existingDatasourceNamesList, existingTitle); + + if (!titleValid.valid) { + return false; } /* Endpoint Validation */ if (!isValidUrl(formValues?.endpoint)) { - validationByField.endpoint.push(dataSourceValidationEndpointNotValid); - formErrorMessages.push(dataSourceValidationEndpointNotValid); + return false; } /* Credential Validation */ @@ -77,46 +74,14 @@ export const performDataSourceFormValidation = ( if (formValues?.auth?.type === AuthType.UsernamePasswordType) { /* Username */ if (!formValues.auth.credentials?.username) { - validationByField.createCredential.username.push(dataSourceValidationUsernameEmpty); - formErrorMessages.push(dataSourceValidationUsernameEmpty); + return false; } /* password */ if (!formValues.auth.credentials?.password) { - validationByField.createCredential.password.push(dataSourceValidationPasswordEmpty); - formErrorMessages.push(dataSourceValidationPasswordEmpty); + return false; } } - return { - formErrors: formErrorMessages, - formErrorsByField: { ...validationByField }, - }; -}; - -export const validateUpdatePassword = (passwords: UpdatePasswordFormType) => { - const validationByField: UpdatePasswordValidation = { - oldPassword: [], - newPassword: [], - confirmNewPassword: [], - }; - - const formErrorMessages: string[] = []; - - if (!passwords.oldPassword) { - validationByField.oldPassword.push(dataSourceValidationOldPasswordEmpty); - formErrorMessages.push(dataSourceValidationOldPasswordEmpty); - } - if (!passwords.newPassword) { - validationByField.newPassword.push(dataSourceValidationNewPasswordEmpty); - formErrorMessages.push(dataSourceValidationNewPasswordEmpty); - } else if (passwords.confirmNewPassword !== passwords.newPassword) { - validationByField.confirmNewPassword.push(dataSourceValidationNoPasswordMatch); - formErrorMessages.push(dataSourceValidationNoPasswordMatch); - } - - return { - formValidationErrors: formErrorMessages, - formValidationErrorsByField: { ...validationByField }, - }; + return true; }; diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index 31ab8237a443..159295a5a808 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -27,6 +27,7 @@ export class DataSourceManagementPlugin opensearchDashboardsSection.registerApp({ id: DSM_APP_ID, title: PLUGIN_NAME, + showExperimentalBadge: true, order: 1, mount: async (params) => { const { mountManagementSection } = await import('./management_app'); diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index c0aa502b5830..a689cb4a593e 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -42,20 +42,12 @@ export interface DataSourceTableItem { export interface ToastMessageItem { id: string; defaultMessage: string; - color: 'primary' | 'success' | 'warning' | 'danger'; - iconType: string; } export type DataSourceManagementContextValue = OpenSearchDashboardsReactContextValue< DataSourceManagementContext >; -export interface UpdatePasswordFormType { - oldPassword: string; - newPassword: string; - confirmNewPassword: string; -} - /* Datasource types */ export enum AuthType { NoAuth = 'no_auth', @@ -63,8 +55,8 @@ export enum AuthType { } export const credentialSourceOptions = [ - { value: AuthType.UsernamePasswordType, inputDisplay: 'Username & Password' }, - { value: AuthType.NoAuth, inputDisplay: 'No authentication' }, + { id: AuthType.NoAuth, label: 'No authentication' }, + { id: AuthType.UsernamePasswordType, label: 'Username & Password' }, ]; export interface DataSourceAttributes extends SavedObjectAttributes { diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.scss b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.scss index 7b134f141d78..f9e5a74d4f35 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.scss +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.scss @@ -9,3 +9,7 @@ margin-bottom: $euiSize; } } + +.mgtSideBarNavItemExperimentalBadge { + margin-left: 10px; +} diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index bb99481fb2b5..6cb6b6aeb164 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -28,7 +28,7 @@ * under the License. */ -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { i18n } from '@osd/i18n'; import { sortBy } from 'lodash'; @@ -40,8 +40,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip, + EuiBadge, } from '@elastic/eui'; import { AppMountParameters } from 'opensearch-dashboards/public'; +import { FormattedMessage } from '@osd/i18n/react'; import { ManagementApp, ManagementSection } from '../../utils'; import './management_sidebar_nav.scss'; @@ -99,7 +101,7 @@ export const ManagementSidebarNav = ({ })); interface TooltipWrapperProps { - text: string; + text: ReactNode | string; tip?: string; } @@ -115,15 +117,28 @@ export const ManagementSidebarNav = ({
); + const TitleWithExperimentalBadge = ({ title }: any) => ( + <> + {title} + + + + + ); + const createNavItem = ( item: T, customParams: Partial> = {} ) => { const iconType = item.euiIconType || item.icon; - + const name = item.showExperimentalBadge ? ( + + ) : ( + item.title + ); return { id: item.id, - name: item.tip ? : item.title, + name: item.tip ? : name, isSelected: item.id === selectedId, icon: iconType ? : undefined, 'data-test-subj': item.id, diff --git a/src/plugins/management/public/types.ts b/src/plugins/management/public/types.ts index be2970427759..a10a0095589c 100644 --- a/src/plugins/management/public/types.ts +++ b/src/plugins/management/public/types.ts @@ -90,6 +90,7 @@ export interface CreateManagementItemArgs { title: string; tip?: string; order?: number; + showExperimentalBadge?: boolean; euiIconType?: EuiIconType; // takes precedence over `icon` property. icon?: string; // URL to image file; fallback if no `euiIconType` } diff --git a/src/plugins/management/public/utils/management_item.ts b/src/plugins/management/public/utils/management_item.ts index 9e90334f36da..de79c626f40f 100644 --- a/src/plugins/management/public/utils/management_item.ts +++ b/src/plugins/management/public/utils/management_item.ts @@ -36,18 +36,28 @@ export class ManagementItem { public readonly title: string; public readonly tip?: string; public readonly order: number; + public readonly showExperimentalBadge: boolean; public readonly euiIconType?: EuiIconType; public readonly icon?: string; public enabled: boolean = true; - constructor({ id, title, tip, order = 100, euiIconType, icon }: CreateManagementItemArgs) { + constructor({ + id, + title, + tip, + order = 100, + euiIconType, + icon, + showExperimentalBadge, + }: CreateManagementItemArgs) { this.id = id; this.title = title; this.tip = tip; this.order = order; this.euiIconType = euiIconType; this.icon = icon; + this.showExperimentalBadge = !!showExperimentalBadge; } disable() {