diff --git a/public/pages/Indices/components/NotificationModal/NotificationModal.tsx b/public/pages/Indices/components/NotificationModal/NotificationModal.tsx new file mode 100644 index 000000000..7bab02f0a --- /dev/null +++ b/public/pages/Indices/components/NotificationModal/NotificationModal.tsx @@ -0,0 +1,323 @@ +// FILE: NotificationsModal.tsx +import React, { ReactChild, useContext, useEffect, useRef, useState } from "react"; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiSpacer, + EuiPanel, + EuiEmptyPrompt, + EuiSmallButton, +} from "@elastic/eui"; +import { ServicesContext } from "../../../../services"; +import { BrowserServices } from "../../../../models/interfaces"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { getUISettings, getNavigationUI, getApplication } from "../../../../services/Services"; +import { CoreStart } from "../../../../../../../src/core/public"; +import useField from "../../../../lib/field"; +import { get } from "lodash"; +import { unstable_batchedUpdates } from "react-dom"; +import { diffJson } from "../../../../utils/helpers"; +import { + getDiffableMapFromPlainList, + getNotifications, + submitNotifications, + transformConfigListToPlainList, +} from "../../../Notifications/hooks"; +import { checkPermissionForSubmitLRONConfig } from "../../../../containers/NotificationConfig"; +import { BREADCRUMBS } from "../../../../utils/constants"; +import { TopNavControlButtonData } from "../../../../../../../src/plugins/navigation/public"; +import { FieldState, ILronPlainConfig } from "../../../Notifications/interface"; +import { AllBuiltInComponents } from "../../../../components/FormGenerator"; +import CustomFormRow from "../../../../components/CustomFormRow"; +import ChannelSelect from "../../../../containers/ChannelSelect"; +import { + ActionType, + ActionTypeMapDescription, + ActionTypeMapTitle, + FieldEnum, + FieldMapLabel, + LABEL_FOR_CONDITION, + VALIDATE_ERROR_FOR_CHANNELS, + getKeyByValue, +} from "../../../Notifications/constant"; +import UnsavedChangesButtons from "./UnsavedChangesButton"; + +export interface NotificationsProps { + onClose: () => void; + visible: boolean; +} + +const Notifications = ({ onClose, visible }: NotificationsProps) => { + const services = useContext(ServicesContext) as BrowserServices; + const coreServices = useContext(CoreServicesContext) as CoreStart; + const [, setIsLoading] = useState(false); + const [submitClicked, setSubmitClicked] = useState(false); + const [noPermission, setNoPermission] = useState(false); + const [permissionForUpdate, setPermissionForUpdate] = useState(false); + const uiSettings = getUISettings(); + const useNewUX = uiSettings.get("home:useNewHomePage"); + + const field = useField({ + values: {} as Partial, + onBeforeChange(name) { + const previousValue = field.getValues(); + if (Array.isArray(name) && name.length === 3 && parseInt(name[1]) > 0) { + const [dataSourceStr, index] = name; + const newProperty = [dataSourceStr, index, FieldEnum.channels]; + if ( + (name[2] === FieldEnum.success || name[2] === FieldEnum.failure) && + !get(previousValue, [dataSourceStr, index, FieldEnum.success]) && + !get(previousValue, [dataSourceStr, index, FieldEnum.failure]) && + (!get(previousValue, newProperty) || !get(previousValue, newProperty).length) + ) { + field.setValue(newProperty, field.getValue([dataSourceStr, `${(parseInt(index) as number) - 1}`, FieldEnum.channels])); + } + } + }, + onChange(name, value) { + if ((name[2] === FieldEnum.success || name[2] === FieldEnum.failure) && !value) { + field.validatePromise(); + } + }, + }); + const destroyRef = useRef(false); + const onSubmit = async (): Promise => { + if (!permissionForUpdate) { + coreServices.notifications.toasts.addDanger({ + title: "You do not have permissions to update notification settings", + text: "Contact your administrator to request permissions.", + }); + return await new Promise((resolve) => setTimeout(() => resolve(undefined), 0)); + } + const { errors, values: notifications } = (await field.validatePromise()) || {}; + setSubmitClicked(!!errors); + if (errors) { + return; + } + setIsLoading(true); + const result = await submitNotifications({ + commonService: services.commonService, + plainConfigsPayload: (notifications.dataSource || []).map((item) => { + if (!item.success && !item.failure) { + return { + ...item, + channels: [], + }; + } + + return item; + }), + }); + if (result && result.ok) { + coreServices.notifications.toasts.addSuccess("Notifications settings for index operations have been successfully updated."); + reloadNotifications(); + onClose(); + } else { + coreServices.notifications.toasts.addDanger(result.error); + } + if (destroyRef.current) { + return; + } + setIsLoading(false); + }; + const reloadNotifications = () => { + setIsLoading(true); + getNotifications({ + commonService: services.commonService, + }) + .then((res) => { + if (res.ok) { + const plainList = transformConfigListToPlainList(res.response.lron_configs.map((item) => item.lron_config)); + const values = { + dataSource: plainList, + } as FieldState; + unstable_batchedUpdates(() => { + field.resetValues(values); + field.setOriginalValues(JSON.parse(JSON.stringify(values))); + }); + } else { + if (res?.body?.status === 403) { + setNoPermission(true); + coreServices.notifications.toasts.addDanger({ + title: "You do not have permissions to view notification settings", + text: "Contact your administrator to request permissions.", + }); + return; + } + coreServices.notifications.toasts.addDanger(res.error); + } + }) + .finally(() => { + if (!destroyRef.current) { + setIsLoading(false); + } + }); + }; + const onCancel = () => { + field.resetValues(field.getOriginalValues()); + onClose(); + }; + useEffect(() => { + reloadNotifications(); + checkPermissionForSubmitLRONConfig({ + services, + }).then((result) => setPermissionForUpdate(result)); + return () => { + destroyRef.current = true; + }; + }, []); + const values = field.getValues(); + const allErrors = Object.entries(field.getErrors()); + + if (!visible) { + return null; + } + + return ( + + + Notification settings + + + Manage channels + + + + + <> + {noPermission ? ( + + Error loading Notification settings} + body={

You do not have permissions to view Notification settings. Contact your administrator to request permissions.

} + /> +
+ ) : ( + + {submitClicked && allErrors.length ? ( + +
    + {allErrors.reduce((total, [key, errors]) => { + const pattern = /^dataSource\.(\d+)\.(\w+)$/; + const matchResult = key.match(pattern); + if (matchResult) { + const index = matchResult[1]; + const itemField = matchResult[2]; + const notificationItem = (field.getValues().dataSource || [])[parseInt(index, 10)]; + const errorMessagePrefix = `${notificationItem.title} — ${ + FieldMapLabel[itemField as keyof typeof FieldMapLabel] + }: `; + return [ + ...total, + ...(errors || []).map((item) => ( +
  • + {errorMessagePrefix} + {item} +
  • + )), + ]; + } + + return total; + }, [] as ReactChild[])} +
+
+ ) : null} + + {(values.dataSource || []).map((record) => { + const { value, onChange, ...others } = field.registerField({ + name: ["dataSource", `${record.index}`, FieldEnum.channels], + rules: [ + { + validator(rule, value) { + const values = field.getValues(); + const item = values.dataSource?.[record.index]; + if (item?.[FieldEnum.failure] || item?.[FieldEnum.success]) { + if (!value || !value.length) { + return Promise.reject(VALIDATE_ERROR_FOR_CHANNELS); + } + } + + return Promise.resolve(""); + }, + }, + ], + }); + return ( + {record.title}} + helpText={ActionTypeMapDescription[getKeyByValue(ActionTypeMapTitle, record.title) as ActionType]} + direction="hoz" + key={record.action_name} + > + <> + + + + + + + + + + + {field.getValue(["dataSource", `${record.index}`, FieldEnum.failure]) || + field.getValue(["dataSource", `${record.index}`, FieldEnum.success]) ? ( + <> + + + + + + ) : null} + + + ); + })} + +
+ )} + +
+ + + +
+ ); +}; + +export default Notifications; diff --git a/public/pages/Indices/components/NotificationModal/UnsavedChangesButton.tsx b/public/pages/Indices/components/NotificationModal/UnsavedChangesButton.tsx new file mode 100644 index 000000000..71e2cb756 --- /dev/null +++ b/public/pages/Indices/components/NotificationModal/UnsavedChangesButton.tsx @@ -0,0 +1,90 @@ +// FILE: UnsavedChangesButtons.tsx + +import React, { useCallback, useRef, useState } from "react"; +import { EuiSmallButton, EuiSmallButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from "@elastic/eui"; +import classNames from "classnames"; + +interface UnsavedChangesButtonsProps { + unsavedCount: number; + formErrorsCount?: number; + onClickCancel?: () => void; + onClickSubmit: () => Promise; + submitButtonDataTestSubj?: string; +} + +const UnsavedChangesButtons: React.FC = ({ + unsavedCount, + formErrorsCount, + onClickCancel, + onClickSubmit, + submitButtonDataTestSubj, +}) => { + const [loading, setLoading] = useState(false); + const destroyRef = useRef(false); + + const onClick = async () => { + setLoading(true); + try { + await onClickSubmit(); + } catch (e) { + } finally { + if (destroyRef.current) { + return; + } + setLoading(false); + } + }; + + const renderCancel = useCallback( + () => ( + + Cancel + + ), + [onClickCancel] + ); + + const renderConfirm = useCallback( + () => ( + + Save + + ), + [onClick, submitButtonDataTestSubj, loading] + ); + + return ( + + <> + {formErrorsCount ? ( + + + {" "} + {formErrorsCount} form errors{" "} + + + ) : null} + {unsavedCount && !formErrorsCount ? ( + + + {" "} + {unsavedCount} unsaved changes{" "} + + + ) : null} + + {renderCancel()} + {renderConfirm()} + + ); +}; + +export default UnsavedChangesButtons; diff --git a/public/pages/Indices/components/NotificationModal/index.ts b/public/pages/Indices/components/NotificationModal/index.ts new file mode 100644 index 000000000..90e24c1ca --- /dev/null +++ b/public/pages/Indices/components/NotificationModal/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import NotificationModal from "./NotificationModal"; + +export default NotificationModal; diff --git a/public/pages/Indices/containers/Indices/Indices.tsx b/public/pages/Indices/containers/Indices/Indices.tsx index 6397a2a73..436886880 100644 --- a/public/pages/Indices/containers/Indices/Indices.tsx +++ b/public/pages/Indices/containers/Indices/Indices.tsx @@ -43,6 +43,7 @@ import MDSEnabledComponent from "../../../../components/MDSEnabledComponent"; import { getApplication, getNavigationUI, getUISettings } from "../../../../services/Services"; import { TopNavControlButtonData, TopNavControlIconData, TopNavControlTextData } from "src/plugins/navigation/public"; import { EuiSpacer } from "@opensearch-project/oui"; +import NotificationModal from "../../components/NotificationModal"; interface IndicesProps extends RouteComponentProps, DataSourceMenuProperties { indexService: IndexService; @@ -63,6 +64,7 @@ interface IndicesState extends DataSourceMenuProperties { showDataStreams: boolean; isDataStreamColumnVisible: boolean; useUpdatedUX: boolean; + isNotificationModalVisible: boolean; } export class Indices extends MDSEnabledComponent { @@ -88,6 +90,7 @@ export class Indices extends MDSEnabledComponent { showDataStreams, isDataStreamColumnVisible: showDataStreams, useUpdatedUX: useUpdatedUX, + isNotificationModalVisible: false, }; this.getIndices = _.debounce(this.getIndices, 500, { leading: true }); @@ -232,6 +235,16 @@ export class Indices extends MDSEnabledComponent { this.setState({ search: DEFAULT_QUERY_PARAMS.search, query: Query.parse(DEFAULT_QUERY_PARAMS.search) }); }; + onCloseNotification = () => { + this.setState({ isNotificationModalVisible: false }); + }; + + toggleNotificationModal = () => { + this.setState((prevState) => ({ + isNotificationModalVisible: !prevState.isNotificationModalVisible, + })); + }; + render() { const { totalIndices, @@ -285,7 +298,7 @@ export class Indices extends MDSEnabledComponent { id: "Notification settings", label: "Notification Settings", fill: false, - // href: `${PLUGIN_NAME}#/create-index`, + run: this.toggleNotificationModal, testId: "notificationSettingsButton", controlType: "button", color: "secondary", @@ -333,6 +346,9 @@ export class Indices extends MDSEnabledComponent { /> + {this.state.isNotificationModalVisible && ( + + )} ) : (