From 9f7c9b6713a212cfdeb6fa26e6bbca2279194ca4 Mon Sep 17 00:00:00 2001 From: Manideep Pabba <109986843+mpabba3003@users.noreply.github.com> Date: Wed, 17 Aug 2022 00:14:11 -0700 Subject: [PATCH] [MD] Datasource Management - creation & listing - UI only (#2128) * data source management - creation & Listing UI only * data source management - creation & Listing UI only * Create/edit data source feature * toggling default value * refactoring code as per review comments * toggling server flag to false Signed-off-by: mpabba3003 Signed-off-by: Kristen Tian --- .../data_source_management/common/index.ts | 2 + .../opensearch_dashboards.json | 2 +- .../public/components/breadcrumbs.ts | 25 +- .../create_button/create_button.tsx | 30 ++ .../public/components/create_button/index.tsx | 6 + .../create_data_source_wizard.tsx | 111 ++++- .../credentials_combo_box.tsx | 49 ++ .../credentials_combox_box/index.ts | 6 + .../components/header/header.tsx | 150 ++++++ .../components/header/index.ts | 6 + .../create_edit_data_source_wizard.tsx | 438 ++++++++++++++++++ .../create_edit_data_source_wizard/index.ts | 6 + .../data_source_table/data_source_table.tsx | 149 +++++- .../edit_data_source/edit_data_source.tsx | 168 +++++++ .../components/edit_data_source/index.ts | 6 + .../public/components/utils.ts | 108 +++++ .../mount_management_section.tsx | 11 +- .../data_source_management/public/plugin.ts | 19 +- .../data_source_management/public/types.ts | 39 +- .../server/tutorials/haproxy_metrics/index.ts | 76 --- 20 files changed, 1297 insertions(+), 110 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/create_button/create_button.tsx create mode 100644 src/plugins/data_source_management/public/components/create_button/index.tsx create mode 100644 src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/credentials_combo_box.tsx create mode 100644 src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/index.ts create mode 100644 src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/header.tsx create mode 100644 src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/index.ts create mode 100644 src/plugins/data_source_management/public/components/create_edit_data_source_wizard/create_edit_data_source_wizard.tsx create mode 100644 src/plugins/data_source_management/public/components/create_edit_data_source_wizard/index.ts create mode 100644 src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx create mode 100644 src/plugins/data_source_management/public/components/edit_data_source/index.ts create mode 100644 src/plugins/data_source_management/public/components/utils.ts delete mode 100644 src/plugins/home/server/tutorials/haproxy_metrics/index.ts diff --git a/src/plugins/data_source_management/common/index.ts b/src/plugins/data_source_management/common/index.ts index e42b0c3fc514..193e2a1875dc 100644 --- a/src/plugins/data_source_management/common/index.ts +++ b/src/plugins/data_source_management/common/index.ts @@ -5,3 +5,5 @@ 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 ded6720dc266..76faa61ba42c 100644 --- a/src/plugins/data_source_management/opensearch_dashboards.json +++ b/src/plugins/data_source_management/opensearch_dashboards.json @@ -3,7 +3,7 @@ "version": "opensearchDashboards", "server": false, "ui": true, - "requiredPlugins": ["management"], + "requiredPlugins": ["management", "credentialManagement"], "optionalPlugins": [], "requiredBundles": ["opensearchDashboardsReact"] } diff --git a/src/plugins/data_source_management/public/components/breadcrumbs.ts b/src/plugins/data_source_management/public/components/breadcrumbs.ts index da1bc530e018..655f7f737661 100644 --- a/src/plugins/data_source_management/public/components/breadcrumbs.ts +++ b/src/plugins/data_source_management/public/components/breadcrumbs.ts @@ -4,14 +4,37 @@ */ import { i18n } from '@osd/i18n'; +import { DataSourceEditPageItem } from '../types'; export function getListBreadcrumbs() { return [ { - text: i18n.translate('indexPatternManagement.dataSources.listBreadcrumb', { + text: i18n.translate('dataSourcesManagement.dataSources.listBreadcrumb', { defaultMessage: 'Data Sources', }), href: `/`, }, ]; } + +export function getCreateBreadcrumbs() { + return [ + ...getListBreadcrumbs(), + { + text: i18n.translate('dataSourcesManagement.dataSources.createBreadcrumb', { + defaultMessage: 'Create data source', + }), + href: `/create`, + }, + ]; +} + +export function getEditBreadcrumbs(dataSource: DataSourceEditPageItem) { + return [ + ...getListBreadcrumbs(), + { + text: dataSource.title, + href: `/${dataSource.id}`, + }, + ]; +} 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 new file mode 100644 index 000000000000..a0da04273d73 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_button/create_button.tsx @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { History } from 'history'; + +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; + +interface Props { + history: History; +} + +export const CreateButton = ({ history }: Props) => { + return ( + history.push('/create')} + iconType="plusInCircle" + > + + + ); +}; diff --git a/src/plugins/data_source_management/public/components/create_button/index.tsx b/src/plugins/data_source_management/public/components/create_button/index.tsx new file mode 100644 index 000000000000..84ddd6830b76 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_button/index.tsx @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { CreateButton } from './create_button'; 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 9d669cf5075e..6c7bf347da00 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 @@ -3,15 +3,112 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiTitle } from '@elastic/eui'; -import React from 'react'; -import { withRouter } from 'react-router-dom'; +import { EuiGlobalToastList, EuiGlobalToastListToast } from '@elastic/eui'; +import React, { useState } from 'react'; +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 { getCreateBreadcrumbs } from '../breadcrumbs'; +import { CreateEditDataSourceWizard } from '../create_edit_data_source_wizard'; +import { MODE_CREATE } from '../../../common'; +import { createSingleDataSource } from '../utils'; + +type CreateDataSourceWizardProps = RouteComponentProps; + +const CreateDataSourceWizard: React.FunctionComponent = ( + props: CreateDataSourceWizardProps +) => { + /* Initialization */ + const { savedObjects, setBreadcrumbs } = useOpenSearchDashboards< + DataSourceManagementContext + >().services; + + const toastLifeTimeMs: number = 6000; + + /* State Variables */ + const [toasts, setToasts] = useState([]); + + /* Set breadcrumb */ + useEffectOnce(() => { + setBreadcrumbs(getCreateBreadcrumbs()); + }); + + /* Handle submit - create data source*/ + const handleSubmit = async ({ + title, + description, + endpoint, + credentialId, + noAuthentication, + }: DataSourceEditPageItem) => { + try { + // TODO: Add rendering spinner + + const references = []; + const attributes = { title, description, endpoint }; + + if (credentialId) { + references.push({ id: credentialId, type: 'credential', name: 'credential' }); + } + const options = { references }; + + await createSingleDataSource(savedObjects.client, attributes, options); + + props.history.push(''); + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.createDataSource.createDataSourceFailMsg', + defaultMessage: 'Creation of the Data Source failed with some errors. Please try it again', + color: 'warning', + iconType: 'alert', + }); + } + }; + + 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 the creation wizard */ + const renderContent = () => { + return ( + + ); + }; + + /* Remove toast on dismiss*/ + const removeToast = (id: string) => { + setToasts(toasts.filter((toast) => toast.id !== id)); + }; -export const CreateDataSourceWizard = () => { return ( - -

{'This is the data source creation page'}

-
+ <> + {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/create_edit_data_source_wizard/components/credentials_combox_box/credentials_combo_box.tsx new file mode 100644 index 000000000000..e6f1585e25df --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/credentials_combo_box.tsx @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { CredentialsComboBoxItem } from '../../../../types'; + +interface CredentialsComboBoxProps { + selectedCredentials: CredentialsComboBoxItem[]; + availableCredentials: CredentialsComboBoxItem[]; + setSelectedCredentials: (selectedOptions: CredentialsComboBoxItem[]) => void; +} + +export const CredentialsComboBox: React.FunctionComponent = ({ + availableCredentials, + selectedCredentials, + setSelectedCredentials, +}: CredentialsComboBoxProps) => { + const onOptionsChanged = (options: EuiComboBoxOptionOption[]) => { + const opts = new Set(); + const selectedCredentialsOptions: CredentialsComboBoxItem[] = []; + if (options?.length) { + options.forEach((rec) => { + opts.add(rec.id); + }); + + availableCredentials.forEach((cred: CredentialsComboBoxItem) => { + if (opts.has(cred.id)) { + selectedCredentialsOptions.push(cred); + } + }); + } + setSelectedCredentials(selectedCredentialsOptions); + }; + + return ( + onOptionsChanged(options)} + isClearable={true} + /> + ); +}; 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/create_edit_data_source_wizard/components/credentials_combox_box/index.ts new file mode 100644 index 000000000000..8ba82a533804 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/credentials_combox_box/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { CredentialsComboBox } from './credentials_combo_box'; 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/create_edit_data_source_wizard/components/header/header.tsx new file mode 100644 index 000000000000..40aa780564f1 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/header.tsx @@ -0,0 +1,150 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; + +import { + EuiBetaBadge, + EuiSpacer, + EuiTitle, + EuiText, + EuiCode, + EuiLink, + EuiFlexItem, + EuiFlexGroup, + EuiToolTip, + EuiButtonIcon, + EuiConfirmModal, +} 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, +}: { + prompt?: React.ReactNode; + dataSourceName: string; + showDeleteIcon: boolean; + onClickDeleteIcon: () => void; + isBeta?: boolean; + docLinks: DocLinksStart; +}) => { + /* State Variables */ + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + + const changeTitle = useOpenSearchDashboards().services.chrome + .docTitle.change; + + const createDataSourceHeader = i18n.translate('dataSourcesManagement.createDataSourceHeader', { + defaultMessage: ` ${dataSourceName}`, + values: { dataSourceName }, + }); + + changeTitle(createDataSourceHeader); + + const renderDeleteButton = () => { + return ( + <> + + { + setIsDeleteModalVisible(true); + }} + iconType="trash" + aria-label="Delete this data source" + /> + + + {isDeleteModalVisible ? ( + { + setIsDeleteModalVisible(false); + }} + onConfirm={() => { + setIsDeleteModalVisible(false); + onClickDeleteIcon(); + }} + cancelButtonText="Cancel" + confirmButtonText="Delete" + defaultFocusedButton="confirm" + > +

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

+

To confirm deletion, click delete button.

+

Note: this action is irrevocable!

+
+ ) : null} + + ); + }; + + return ( + + +
+ +

+ {createDataSourceHeader} + {isBeta ? ( + <> + + + ) : null} +

+
+ + +

+ 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/header/index.ts b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/index.ts new file mode 100644 index 000000000000..3c25d4c42f03 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/components/header/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { Header } from './header'; 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 new file mode 100644 index 000000000000..f01d00f417c5 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/create_edit_data_source_wizard.tsx @@ -0,0 +1,438 @@ +/* + * 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 } 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[]; +} + +const defaultValidation: CreateEditDataSourceValidation = { + title: [], + description: [], + endpoint: [], +}; + +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: [], + }; + const formErrorMessages: string[] = []; + if (!this.state.dataSourceTitle) { + validationByField.title.push('Title should not be empty'); + formErrorMessages.push('Title should not be empty'); + } + if (!this.state.dataSourceDescription) { + validationByField.description.push('Description should not be empty'); + formErrorMessages.push('Description should not be empty'); + } + if (!this.state.endpoint) { + validationByField.endpoint.push('Endpoint should not be empty'); + formErrorMessages.push('Endpoint should not be empty'); + } + 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(); + } + }); + }; + + 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 }); + }; + + 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()} + + + + { + this.setState({ + noAuthentication: !this.state.noAuthentication, + }); + }} + compressed + /> + + + {/* 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/create_edit_data_source_wizard/index.ts b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/index.ts new file mode 100644 index 000000000000..a253a999f1e4 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_edit_data_source_wizard/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { CreateEditDataSourceWizard } from './create_edit_data_source_wizard'; 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 24ecfa8e7d49..a85cc1ab178f 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 @@ -3,22 +3,149 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiTitle } from '@elastic/eui'; -import React from 'react'; -import { withRouter } from 'react-router-dom'; +import { + EuiBadge, + EuiBadgeGroup, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiInMemoryTable, + EuiPageContent, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { useEffectOnce } from 'react-use'; import { getListBreadcrumbs } from '../breadcrumbs'; -import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { DataSourceManagementContext } from '../../types'; +import { + reactRouterNavigate, + useOpenSearchDashboards, +} from '../../../../opensearch_dashboards_react/public'; +import { DataSourceManagementContext, DataSourceTableItem } from '../../types'; +import { CreateButton } from '../create_button'; +import { getDataSources } from '../utils'; -export const DataSourceTable = () => { - const { setBreadcrumbs } = useOpenSearchDashboards().services; +/* Table config */ +const pagination = { + initialPageSize: 10, + pageSizeOptions: [5, 10, 25, 50], +}; + +const sorting = { + sort: { + field: 'title', + direction: 'asc' as const, + }, +}; + +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', +}); + +export const DataSourceTable = ({ history }: RouteComponentProps) => { + const { setBreadcrumbs, savedObjects } = useOpenSearchDashboards< + DataSourceManagementContext + >().services; + + /* Component state variables */ + const [dataSources, setDataSources] = useState([]); + + /* 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); + })(); + }); + + /* Table columns */ + const columns = [ + { + field: 'title', + name: 'Datasource', + render: ( + name: string, + index: { + id: string; + tags?: Array<{ + key: string; + name: string; + }>; + } + ) => ( + <> + + {name} + +   + + {index.tags && + index.tags.map(({ key: tagKey, name: tagName }) => ( + {tagName} + ))} + + + ), + dataType: 'string' as const, + sortable: ({ sort }: { sort: string }) => sort, + }, + ]; - setBreadcrumbs(getListBreadcrumbs()); + /* Create Data Source button */ + const createButton = ; + /* UI Elements */ return ( - -

{'This is the landing page, going to list data sources here...'}

-
+ + + + +

{title}

+
+ + +

+ +

+
+
+ {createButton} +
+ + +
); }; 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 new file mode 100644 index 000000000000..91d30b373887 --- /dev/null +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx @@ -0,0 +1,168 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 { 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 { deleteDataSourceById, getDataSourceById, updateDataSourceById } from '../utils'; +import { getEditBreadcrumbs } from '../breadcrumbs'; + +const defaultDataSource: DataSourceEditPageItem = { + id: '', + title: '', + description: '', + endpoint: '', + credentialId: '', + noAuthentication: false, +}; + +const EditDataSource: React.FunctionComponent> = ( + props: RouteComponentProps<{ id: string }> +) => { + /* Initialization */ + const { savedObjects, setBreadcrumbs } = useOpenSearchDashboards< + DataSourceManagementContext + >().services; + + /* State Variables */ + const [dataSource, setDataSource] = useState(defaultDataSource); + + const toastLifeTimeMs: number = 6000; + + /* State Variables */ + const [toasts, setToasts] = useState([]); + + /* Fetch data source by id*/ + useEffectOnce(() => { + (async function () { + try { + const fetchDataSourceById = await getDataSourceById( + props.match.params.id, + savedObjects.client + ); + + if (fetchDataSourceById) { + setDataSource(fetchDataSourceById); + setBreadcrumbs(getEditBreadcrumbs(fetchDataSourceById)); + } + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.editDataSource.editDataSourceFailMsg', + defaultMessage: 'Unable to find the Data Source. Please try it again.', + color: 'warning', + iconType: 'alert', + }); + + props.history.push(''); + } + })(); + }); + + /* Handle submit - create data source*/ + const handleSubmit = async ({ + title, + description, + endpoint, + id, + credentialId, + noAuthentication, + }: DataSourceEditPageItem) => { + try { + // TODO: Add rendering spanner https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2050 + + const references = []; + const attributes = { title, description, endpoint }; + + if (credentialId) { + references.push({ id: credentialId, type: 'credential', name: 'credential' }); + } + const options = { references }; + + await updateDataSourceById(savedObjects.client, id, attributes, options); + + props.history.push(''); + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.editDataSource.editDataSourceFailMsg', + defaultMessage: 'Updating the Data Source failed with some errors. Please try it again.', + color: 'warning', + iconType: 'alert', + }); + } + }; + + const handleDisplayToastMessage = ({ id, defaultMessage, color, iconType }: ToastMessageItem) => { + if (id && defaultMessage && color && iconType) { + const failureMsg = ; + setToasts([ + ...toasts, + { + title: failureMsg, + id: failureMsg.props.id, + color, + iconType, + }, + ]); + } + }; + + /* Handle delete - data source*/ + const handleDelete = async () => { + try { + await deleteDataSourceById(props.match.params.id, savedObjects.client); + props.history.push(''); + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.editDataSource.deleteDataSourceFailMsg', + defaultMessage: 'Unable to delete the Data Source due to some errors. Please try it again.', + color: 'warning', + iconType: 'alert', + }); + } + }; + + /* Render the edit wizard */ + const renderContent = () => { + return ( + + ); + }; + + /* Remove toast on dismiss*/ + const removeToast = (id: string) => { + setToasts(toasts.filter((toast) => toast.id !== id)); + }; + + if (!dataSource?.id || !dataSource?.title) { + return

Data Source not found!

; + } + + return ( + <> + {renderContent()} + { + removeToast(id); + }} + toastLifeTimeMs={toastLifeTimeMs} + /> + + ); +}; + +export const EditDataSourceWithRouter = withRouter(EditDataSource); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/index.ts b/src/plugins/data_source_management/public/components/edit_data_source/index.ts new file mode 100644 index 000000000000..815c4f53b999 --- /dev/null +++ b/src/plugins/data_source_management/public/components/edit_data_source/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { EditDataSourceWithRouter } from './edit_data_source'; diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts new file mode 100644 index 000000000000..b51942537085 --- /dev/null +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsClientContract } from 'src/core/public'; + +export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { + return ( + savedObjectsClient + .find({ + type: 'data-source', + fields: ['id', 'type', 'title'], + perPage: 10000, + }) + .then((response) => + response.savedObjects + .map((source) => { + const id = source.id; + const title = source.get('title'); + + return { + id, + title, + sort: `${title}`, + }; + }) + .sort((a, b) => { + if (a.sort < b.sort) { + return -1; + } else if (a.sort > b.sort) { + return 1; + } else { + return 0; + } + }) + ) || [] + ); +} + +export async function getDataSourceById( + id: string, + savedObjectsClient: SavedObjectsClientContract +) { + return ( + savedObjectsClient.get('data-source', id).then((response) => { + const attributes: any = response?.attributes || {}; + let credentialId: string = ''; + if (response?.references?.length) { + response.references.forEach((rec) => { + if (rec.name === 'credential') { + credentialId = rec.id; + } + }); + } + return { + id: response.id, + title: attributes.title, + endpoint: attributes.endpoint, + description: attributes.description || '', + credentialId, + noAuthentication: false, // TODO: get noAuthentication from response + }; + }) || null + ); +} + +export async function createSingleDataSource( + savedObjectsClient: SavedObjectsClientContract, + attributes: { title: string; description: string; endpoint: string }, + options?: { references: any[] } +) { + return savedObjectsClient.create('data-source', attributes, options); +} + +export async function updateDataSourceById( + savedObjectsClient: SavedObjectsClientContract, + id: string, + attributes: { title: string; description: string; endpoint: string }, + options?: { references: any[] } +) { + return savedObjectsClient.update('data-source', id, attributes, options); +} + +export async function deleteDataSourceById( + id: string, + savedObjectsClient: SavedObjectsClientContract +) { + return savedObjectsClient.delete('data-source', id); +} + +export async function getExistingCredentials(savedObjectsClient: SavedObjectsClientContract) { + const type: string = 'credential'; + const fields: string[] = ['id', 'title']; + 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'); + return { + id, + title, + label: `${title}`, + }; + }) || [] + ); +} 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 5ba15c1c2a62..e82dc171a76f 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 @@ -10,15 +10,21 @@ import { i18n } from '@osd/i18n'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { ManagementAppMountParams } from '../../../management/public'; import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; import { CreateDataSourceWizardWithRouter } from '../components/create_data_source_wizard'; import { DataSourceTableWithRouter } from '../components/data_source_table'; import { DataSourceManagementContext } from '../types'; +import { EditDataSourceWithRouter } from '../components/edit_data_source'; + +export interface DataSourceManagementStartDependencies { + data: DataPublicPluginStart; +} export async function mountManagementSection( - getStartServices: StartServicesAccessor, + getStartServices: StartServicesAccessor, params: ManagementAppMountParams ) { const [ @@ -52,6 +58,9 @@ export async function mountManagementSection( + + + diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index fc5d0db686e7..31ab8237a443 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -4,14 +4,19 @@ */ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { DataSourceManagementPluginStart, DataSourceManagementSetupDependencies } from './types'; import { PLUGIN_NAME } from '../common'; -const IPM_APP_ID = 'dataSources'; +import { ManagementSetup } from '../../management/public'; + +export interface DataSourceManagementSetupDependencies { + management: ManagementSetup; +} + +const DSM_APP_ID = 'dataSources'; export class DataSourceManagementPlugin - implements Plugin { + implements Plugin { public setup(core: CoreSetup, { management }: DataSourceManagementSetupDependencies) { const opensearchDashboardsSection = management.sections.section.opensearchDashboards; @@ -20,9 +25,9 @@ export class DataSourceManagementPlugin } opensearchDashboardsSection.registerApp({ - id: IPM_APP_ID, + id: DSM_APP_ID, title: PLUGIN_NAME, - order: 0, + order: 1, mount: async (params) => { const { mountManagementSection } = await import('./management_app'); @@ -31,9 +36,7 @@ export class DataSourceManagementPlugin }); } - public start(core: CoreStart): DataSourceManagementPluginStart { - return {}; - } + public start(core: CoreStart) {} public stop() {} } diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index 1ca1231d5591..b66d5205330a 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -13,15 +13,12 @@ import { DocLinksStart, HttpSetup, } from 'src/core/public'; -import { ManagementAppMountParams, ManagementSetup } from 'src/plugins/management/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; +import { OpenSearchDashboardsReactContextValue } from '../../opensearch_dashboards_react/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataSourceManagementPluginStart {} -export interface DataSourceManagementSetupDependencies { - management: ManagementSetup; -} - export interface DataSourceManagementContext { chrome: ChromeStart; application: ApplicationStart; @@ -33,3 +30,35 @@ export interface DataSourceManagementContext { docLinks: DocLinksStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; } + +export interface DataSourceTableItem { + id: string; + title: string; + sort: string; +} + +export interface CredentialsComboBoxItem { + id: string; + title: string; + label: string; +} + +export interface DataSourceEditPageItem { + id: string; + title: string; + description: string; + endpoint: string; + credentialId: string; + noAuthentication: boolean; +} + +export interface ToastMessageItem { + id: string; + defaultMessage: string; + color: 'primary' | 'success' | 'warning' | 'danger'; + iconType: string; +} + +export type DataSourceManagementContextValue = OpenSearchDashboardsReactContextValue< + DataSourceManagementContext +>; diff --git a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts deleted file mode 100644 index be275726877a..000000000000 --- a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@osd/i18n'; -import { TutorialsCategory } from '../../services/tutorials'; -import { onPremInstructions } from '../instructions/metricbeat_instructions'; -import { - TutorialContext, - TutorialSchema, -} from '../../services/tutorials/lib/tutorials_registry_types'; - -export function haproxyMetricsSpecProvider(context: TutorialContext): TutorialSchema { - const moduleName = 'haproxy'; - return { - id: 'haproxyMetrics', - name: i18n.translate('home.tutorials.haproxyMetrics.nameTitle', { - defaultMessage: 'HAProxy metrics', - }), - moduleName, - isBeta: false, - category: TutorialsCategory.METRICS, - shortDescription: i18n.translate('home.tutorials.haproxyMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the HAProxy server.', - }), - longDescription: i18n.translate('home.tutorials.haproxyMetrics.longDescription', { - defaultMessage: - 'The `haproxy` Metricbeat module fetches internal metrics from HAProxy. \ -[Learn more]({learnMoreLink}).', - values: { - learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-haproxy.html', - }, - }), - euiIconType: 'logoHAproxy', - artifacts: { - application: { - label: i18n.translate('home.tutorials.haproxyMetrics.artifacts.application.label', { - defaultMessage: 'Discover', - }), - path: '/app/discover#/', - }, - dashboards: [], - exportedFields: { - documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-haproxy.html', - }, - }, - completionTimeMinutes: 10, - onPrem: onPremInstructions(moduleName, context), - }; -}